diff --git a/radicale/__init__.py b/radicale/__init__.py index 7ececaf..10def3a 100644 --- a/radicale/__init__.py +++ b/radicale/__init__.py @@ -311,7 +311,12 @@ class Application: headers = {"Location": redirect} return response(status, headers) - is_authenticated = self.is_authenticated(user, password) + if user and not storage.is_safe_path_component(user): + # Prevent usernames like "user/calendar.ics" + self.logger.info("Refused unsafe username: %s", user) + is_authenticated = False + else: + is_authenticated = self.is_authenticated(user, password) is_valid_user = is_authenticated or not user # Get content diff --git a/radicale/rights.py b/radicale/rights.py index e4d780a..b0482e4 100644 --- a/radicale/rights.py +++ b/radicale/rights.py @@ -43,6 +43,8 @@ from configparser import ConfigParser from importlib import import_module from io import StringIO +from . import storage + def load(configuration, logger): """Load the rights manager chosen in configuration.""" @@ -103,6 +105,9 @@ class Rights(BaseRights): def authorized(self, user, collection, permission): user = user or '' + if user and not storage.is_safe_path_component(user): + # Prevent usernames like "user/calendar.ics" + raise ValueError("Unsafe username") collection_url = collection.path.rstrip("/") or "/" if collection_url in (".well-known/carddav", ".well-known/caldav"): return permission == "r" diff --git a/radicale/storage.py b/radicale/storage.py index f8c8d9f..d53e711 100644 --- a/radicale/storage.py +++ b/radicale/storage.py @@ -103,6 +103,15 @@ def get_etag(text): return '"%s"' % etag.hexdigest() +def is_safe_path_component(path): + """Check if path is a single component of a path. + + Check that the path is safe to join too. + + """ + return path and "/" not in path and path not in (".", "..") + + def sanitize_path(path): """Make path absolute with leading slash to prevent access to other data.