From b93842b10c9bb5d1e74e0cdfdf14eddb5b9e6c84 Mon Sep 17 00:00:00 2001 From: Unrud Date: Sat, 15 Jan 2022 22:32:37 +0100 Subject: [PATCH] Redirect GET and HEAD requests to sanitized path --- radicale/app/__init__.py | 24 ++++++++++++++++-------- radicale/app/get.py | 9 +++------ radicale/tests/test_base.py | 11 +++++++++-- radicale/web/internal_data/fn.js | 2 +- 4 files changed, 29 insertions(+), 17 deletions(-) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index d473d76..6ff6840 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -161,6 +161,9 @@ class Application(ApplicationPartDelete, ApplicationPartHead, # Return response content return status_text, list(headers.items()), answers + time_begin = datetime.datetime.now() + request_method = environ["REQUEST_METHOD"].upper() + unsafe_path = environ.get("PATH_INFO", "") remote_host = "unknown" if environ.get("REMOTE_HOST"): remote_host = repr(environ["REMOTE_HOST"]) @@ -175,11 +178,9 @@ class Application(ApplicationPartDelete, ApplicationPartHead, depthinfo = "" if environ.get("HTTP_DEPTH"): depthinfo = " with depth %r" % environ["HTTP_DEPTH"] - time_begin = datetime.datetime.now() - logger.info( - "%s request for %r%s received from %s%s", - environ["REQUEST_METHOD"], environ.get("PATH_INFO", ""), depthinfo, - remote_host, remote_useragent) + logger.info("%s request for %r%s received from %s%s", + request_method, unsafe_path, depthinfo, + remote_host, remote_useragent) logger.debug("Request headers:\n%s", pprint.pformat(self._scrub_headers(environ))) @@ -191,12 +192,19 @@ class Application(ApplicationPartDelete, ApplicationPartHead, logger.debug("Base prefix: %r", base_prefix) # Sanitize request URI (a WSGI server indicates with an empty path, # that the URL targets the application root without a trailing slash) - path = pathutils.sanitize_path(environ.get("PATH_INFO", "")) + path = pathutils.sanitize_path(unsafe_path) + if unsafe_path != path and request_method in ["GET", "HEAD"]: + location = base_prefix + path + logger.info("Redirecting to sanitized path: %r ==> %r", + base_prefix + unsafe_path, location) + return response( + client.MOVED_PERMANENTLY, + {"Location": location, "Content-Type": "text/plain"}, + "Redirected to %s" % location) logger.debug("Sanitized path: %r", path) # Get function corresponding to method - function = getattr( - self, "do_%s" % environ["REQUEST_METHOD"].upper(), None) + function = getattr(self, "do_%s" % request_method, None) if not function: return response(*httputils.METHOD_NOT_ALLOWED) diff --git a/radicale/app/get.py b/radicale/app/get.py index 255a3f1..3b26815 100644 --- a/radicale/app/get.py +++ b/radicale/app/get.py @@ -62,13 +62,10 @@ class ApplicationPartGet(ApplicationBase): """Manage GET request.""" # Redirect to .web if the root URL is requested if not pathutils.strip_path(path): - web_path = ".web" - if not environ.get("PATH_INFO"): - web_path = posixpath.join(posixpath.basename(base_prefix), - web_path) + location = ".web" return (client.FOUND, - {"Location": web_path, "Content-Type": "text/plain"}, - "Redirected to %s" % web_path) + {"Location": location, "Content-Type": "text/plain"}, + "Redirected to %s" % location) # Dispatch .web URL to web module if path == "/.web" or path.startswith("/.web/"): return self._web.get(environ, base_prefix, path, user) diff --git a/radicale/tests/test_base.py b/radicale/tests/test_base.py index 65f2556..9b16ba6 100644 --- a/radicale/tests/test_base.py +++ b/radicale/tests/test_base.py @@ -60,8 +60,15 @@ permissions: RrWw""") """GET request at "/" with SCRIPT_NAME.""" _, answer = self.get("/", check=302, SCRIPT_NAME="/radicale") assert answer == "Redirected to .web" - _, answer = self.get("", check=302, SCRIPT_NAME="/radicale") - assert answer == "Redirected to radicale/.web" + + def test_sanitized_path(self) -> None: + """GET request with unsanitized paths.""" + for path, sane_path in [("//", "/"), ("", "/"), ("/a//b", "/a/b"), + ("/a//b/", "/a/b/")]: + _, answer = self.get(path, check=301) + assert answer == "Redirected to %s" % sane_path + _, answer = self.get(path, check=301, SCRIPT_NAME="/radicale") + assert answer == "Redirected to /radicale%s" % sane_path def test_add_event(self) -> None: """Add an event.""" diff --git a/radicale/web/internal_data/fn.js b/radicale/web/internal_data/fn.js index a61fd32..82651a3 100644 --- a/radicale/web/internal_data/fn.js +++ b/radicale/web/internal_data/fn.js @@ -28,7 +28,7 @@ const SERVER = location.origin; * @const * @type {string} */ -const ROOT_PATH = location.pathname.replace(new RegExp("/+[^/]+/*(/index\\.html?)?$"), "") + '/'; +const ROOT_PATH = (new URL("..", location.href)).pathname; /** * Regex to match and normalize color