fix circular import related to user/path value check

This commit is contained in:
Peter Bieringer
2026-04-23 08:33:53 +02:00
parent 56724496d2
commit 1495849b61
4 changed files with 74 additions and 61 deletions

View File

@@ -42,6 +42,7 @@ from http import client
from typing import Iterable, List, Mapping, Tuple, Union from typing import Iterable, List, Mapping, Tuple, Union
from radicale import config, httputils, log, pathutils, types, utils 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.base import ApplicationBase
from radicale.app.delete import ApplicationPartDelete from radicale.app.delete import ApplicationPartDelete
from radicale.app.get import ApplicationPartGet 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) 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: else:
logger.debug("Called by reverse proxy, cannot remove base prefix %r from path: %r as not matching", base_prefix, path) 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) logger.error("request contains invalid path: %r (not compliant to %r)", path, self._validate_path_value)
return response(*httputils.BAD_REQUEST) return response(*httputils.BAD_REQUEST)
@@ -515,7 +516,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
self.configuration, environ, base64.b64decode( self.configuration, environ, base64.b64decode(
authorization.encode("ascii"))).split(":", 1) 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 info = "not compliant to %r" % self._validate_user_value
user = "" user = ""
else: else:

View File

@@ -45,6 +45,70 @@ USER_WHITELIST_UNICODE: list = ["-", ".", "@", "_"] # from USER_PATTERN_STRICT
PATH_WHITELIST_UNICODE: list = ["-", ".", "@", "_", "/", "~"] # from PATH_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: class ApplicationBase:
configuration: config.Configuration configuration: config.Configuration
@@ -117,59 +181,6 @@ class ApplicationBase:
content = self._xml_response(xmlutils.webdav_error(human_tag)) content = self._xml_response(xmlutils.webdav_error(human_tag))
return status, headers, content, None 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: class Access:
"""Helper class to check access rights of an item""" """Helper class to check access rights of an item"""

View File

@@ -25,6 +25,7 @@ from http import client
from urllib.parse import unquote, urlparse from urllib.parse import unquote, urlparse
from radicale import httputils, pathutils, storage, types from radicale import httputils, pathutils, storage, types
from radicale.app import base as app_base
from radicale.app.base import Access, ApplicationBase from radicale.app.base import Access, ApplicationBase
from radicale.log import logger from radicale.log import logger
@@ -82,7 +83,7 @@ class ApplicationPartMove(ApplicationBase):
if not access.check("w"): if not access.check("w"):
return httputils.NOT_ALLOWED return httputils.NOT_ALLOWED
to_path = pathutils.sanitize_path(to_url.path) 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) logger.warning("request contains invalid path: %r (not compliant to %r)", to_path, self._validate_path_value)
return httputils.BAD_REQUEST return httputils.BAD_REQUEST
if not (to_path + "/").startswith(base_prefix + "/"): if not (to_path + "/").startswith(base_prefix + "/"):

View File

@@ -29,7 +29,6 @@ from urllib.parse import parse_qs
from radicale import (config, httputils, pathutils, rights, storage, types, from radicale import (config, httputils, pathutils, rights, storage, types,
utils) utils)
from radicale.app.base import ApplicationBase
from radicale.log import logger from radicale.log import logger
INTERNAL_TYPES: Sequence[str] = ("csv", "files", "none") 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) return utils.load_plugin(INTERNAL_TYPES, "sharing", "Sharing", BaseSharing, configuration)
class BaseSharing(ApplicationBase): class BaseSharing:
_storage: storage.BaseStorage _storage: storage.BaseStorage
_rights: rights.BaseRights _rights: rights.BaseRights
@@ -509,6 +508,7 @@ class BaseSharing(ApplicationBase):
# *** POST API *** # *** POST API ***
def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse: def post(self, environ: types.WSGIEnviron, base_prefix: str, path: str, user: str) -> types.WSGIResponse:
# Late import to avoid circular dependency in config # Late import to avoid circular dependency in config
from radicale.app import base as app_base
from radicale.app.base import Access from radicale.app.base import Access
"""POST request. """POST request.
@@ -725,19 +725,19 @@ class BaseSharing(ApplicationBase):
logger.warning(api_info + ": unsupported " + key) logger.warning(api_info + ": unsupported " + key)
return httputils.bad_request("Invalid value for PathOrToken") return httputils.bad_request("Invalid value for PathOrToken")
else: 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) 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") return httputils.bad_request("Invalid value for PathOrToken")
if not request_data[key].endswith("/"): if not request_data[key].endswith("/"):
return httputils.bad_request("PathOrToken not ending with /") return httputils.bad_request("PathOrToken not ending with /")
elif key == "PathMapped": 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) 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") return httputils.bad_request("Invalid value for PathMapped")
elif not request_data[key].endswith("/"): elif not request_data[key].endswith("/"):
return httputils.bad_request("PathMapped not ending with /") return httputils.bad_request("PathMapped not ending with /")
elif key == "User": 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) 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") return httputils.bad_request("Invalid value for User")