From 1495849b616db078b29e0940d807311309609a1e Mon Sep 17 00:00:00 2001 From: Peter Bieringer Date: Thu, 23 Apr 2026 08:33:53 +0200 Subject: [PATCH] fix circular import related to user/path value check --- radicale/app/__init__.py | 5 +- radicale/app/base.py | 117 +++++++++++++++++++---------------- radicale/app/move.py | 3 +- radicale/sharing/__init__.py | 10 +-- 4 files changed, 74 insertions(+), 61 deletions(-) diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 96a441ef..46814e44 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -42,6 +42,7 @@ from http import client from typing import Iterable, List, Mapping, Tuple, Union from radicale import config, httputils, log, pathutils, types, utils +from radicale.app import base as app_base from radicale.app.base import ApplicationBase from radicale.app.delete import ApplicationPartDelete from radicale.app.get import ApplicationPartGet @@ -482,7 +483,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead, logger.warning("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching (may cause authentication issues using internal WebUI)", base_prefix, path) else: logger.debug("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching", base_prefix, path) - if not self._check_path_format(path): + if not app_base._check_path_format(self._storage, path, self._validate_path_value): logger.error("request contains invalid path: %r (not compliant to %r)", path, self._validate_path_value) return response(*httputils.BAD_REQUEST) @@ -515,7 +516,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead, self.configuration, environ, base64.b64decode( authorization.encode("ascii"))).split(":", 1) - if login and not self._check_user_format(login): + if login and not app_base._check_user_format(self._storage, login, self._validate_user_value): info = "not compliant to %r" % self._validate_user_value user = "" else: diff --git a/radicale/app/base.py b/radicale/app/base.py index fd4d9342..63993a9a 100644 --- a/radicale/app/base.py +++ b/radicale/app/base.py @@ -45,6 +45,70 @@ USER_WHITELIST_UNICODE: list = ["-", ".", "@", "_"] # from USER_PATTERN_STRICT PATH_WHITELIST_UNICODE: list = ["-", ".", "@", "_", "/", "~"] # from PATH_PATTERN_STRICT +def _check_format(self: storage.BaseStorage, + string: str, + blacklist_minimal: list[str], + whitelist_unicode: list[str], + validation_type: str, + ) -> bool: + check_minimal = (validation_type == "minimal") + check_unicode_letter = (validation_type == "unicode-letter") + check_no_unicode = (validation_type == "no-unicode") + logger.trace("_check_format investigate %r (validation_type=%r check_minimal=%s check_unicode_letter=%s check_no_unicode=%s)", string, validation_type, check_minimal, check_unicode_letter, check_no_unicode) + if not self._supports_trailing_whitespace and string.endswith(' '): + return False + for c in string: + if c <= chr(31) or (c >= chr(127) and c <= chr(159)): + # ASCII: control char + return False + if unicodedata.category(c)[0] == "C": + # https://unicodeplus.com/category + # Unicode: control + return False + if check_minimal or not self._supports_problematic_chars: + if c in blacklist_minimal: + logger.trace("_check_format found %r", c) + return False + if check_unicode_letter: + if c not in whitelist_unicode: + if unicodedata.category(c)[0] != "L": + return False + if check_no_unicode: + if ord(c) > 255: + return False + return True + + +def _check_user_format(self: storage.BaseStorage, + user: str, + validation_type: str + ) -> bool: + if validation_type == "strict": + return (re.search(USER_PATTERN_STRICT_RE, user) is not None) + else: + return _check_format(self, + user, + USER_BLACKLIST_MINIMAL, + USER_WHITELIST_UNICODE, + validation_type, + ) + + +def _check_path_format(self: storage.BaseStorage, + path: str, + validation_type: str + ) -> bool: + if validation_type == "strict": + return (re.search(PATH_PATTERN_STRICT_RE, path) is not None) + else: + return _check_format(self, + path, + PATH_BLACKLIST_MINIMAL, + PATH_WHITELIST_UNICODE, + validation_type, + ) + + class ApplicationBase: configuration: config.Configuration @@ -117,59 +181,6 @@ class ApplicationBase: content = self._xml_response(xmlutils.webdav_error(human_tag)) return status, headers, content, None - def _check_format(self, - string: str, - blacklist_minimal: list[str], - whitelist_unicode: list[str], - validation_type: str, - ) -> bool: - check_minimal = (validation_type == "minimal") - check_unicode_letter = (validation_type == "unicode-letter") - check_no_unicode = (validation_type == "no-unicode") - logger.trace("_check_format investigate %r (validation_type=%r check_minimal=%s check_unicode_letter=%s check_no_unicode=%s)", string, validation_type, check_minimal, check_unicode_letter, check_no_unicode) - if not self._storage._supports_trailing_whitespace and string.endswith(' '): - return False - for c in string: - if c <= chr(31) or (c >= chr(127) and c <= chr(159)): - # ASCII: control char - return False - if unicodedata.category(c)[0] == "C": - # https://unicodeplus.com/category - # Unicode: control - return False - if check_minimal or not self._storage._supports_problematic_chars: - if c in blacklist_minimal: - logger.trace("_check_format found %r", c) - return False - if check_unicode_letter: - if c not in whitelist_unicode: - if unicodedata.category(c)[0] != "L": - return False - if check_no_unicode: - if ord(c) > 255: - return False - return True - - def _check_user_format(self, user: str) -> bool: - if self._validate_user_value == "strict": - return (re.search(USER_PATTERN_STRICT_RE, user) is not None) - else: - return self._check_format(user, - USER_BLACKLIST_MINIMAL, - USER_WHITELIST_UNICODE, - self._validate_user_value, - ) - - def _check_path_format(self, path: str) -> bool: - if self._validate_path_value == "strict": - return (re.search(PATH_PATTERN_STRICT_RE, path) is not None) - else: - return self._check_format(path, - PATH_BLACKLIST_MINIMAL, - PATH_WHITELIST_UNICODE, - self._validate_path_value, - ) - class Access: """Helper class to check access rights of an item""" diff --git a/radicale/app/move.py b/radicale/app/move.py index 7d0376ee..f16368f4 100644 --- a/radicale/app/move.py +++ b/radicale/app/move.py @@ -25,6 +25,7 @@ from http import client from urllib.parse import unquote, urlparse from radicale import httputils, pathutils, storage, types +from radicale.app import base as app_base from radicale.app.base import Access, ApplicationBase from radicale.log import logger @@ -82,7 +83,7 @@ class ApplicationPartMove(ApplicationBase): if not access.check("w"): return httputils.NOT_ALLOWED to_path = pathutils.sanitize_path(to_url.path) - if not self._check_path_format(to_path): + if not app_base._check_path_format(self._storage, to_path, self._validate_path_value): logger.warning("request contains invalid path: %r (not compliant to %r)", to_path, self._validate_path_value) return httputils.BAD_REQUEST if not (to_path + "/").startswith(base_prefix + "/"): diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 6b837c11..ba2c5f6b 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -29,7 +29,6 @@ from urllib.parse import parse_qs from radicale import (config, httputils, pathutils, rights, storage, types, utils) -from radicale.app.base import ApplicationBase from radicale.log import logger INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none") @@ -132,7 +131,7 @@ def load(configuration: "config.Configuration") -> "BaseSharing": return utils.load_plugin(INTERNAL_TYPES, "sharing", "Sharing", BaseSharing, configuration) -class BaseSharing(ApplicationBase): +class BaseSharing: _storage: storage.BaseStorage _rights: rights.BaseRights @@ -509,6 +508,7 @@ class BaseSharing(ApplicationBase): # *** POST API *** def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse: # Late import to avoid circular dependency in config + from radicale.app import base as app_base from radicale.app.base import Access """POST request. @@ -725,19 +725,19 @@ class BaseSharing(ApplicationBase): logger.warning(api_info + ": unsupported " + key) return httputils.bad_request("Invalid value for PathOrToken") else: - if not self._check_path_format(request_data[key]): + if not app_base._check_path_format(self._storage, request_data[key], self._validate_path_value): logger.warning("%s: invalid %r: %r (not compliant to %r)", api_info, key, request_data[key], self._validate_path_value) return httputils.bad_request("Invalid value for PathOrToken") if not request_data[key].endswith("/"): return httputils.bad_request("PathOrToken not ending with /") elif key == "PathMapped": - if not self._check_path_format(request_data[key]): + if not app_base._check_path_format(self._storage, request_data[key], self._validate_path_value): logger.warning("%s: invalid %r: %r (not compliant to %r)", api_info, key, request_data[key], self._validate_path_value) return httputils.bad_request("Invalid value for PathMapped") elif not request_data[key].endswith("/"): return httputils.bad_request("PathMapped not ending with /") elif key == "User": - if not self._check_user_format(request_data[key]): + if not app_base._check_user_format(self._storage, request_data[key], self._validate_user_value): logger.warning("%s: invalid %r: %r (not compliant to %r)", api_info, key, request_data[key], self._validate_user_value) return httputils.bad_request("Invalid value for User")