use new format checker for path/user

This commit is contained in:
Peter Bieringer
2026-04-21 18:54:28 +02:00
parent af5121ab34
commit 53d5a89165
4 changed files with 77 additions and 11 deletions

View File

@@ -466,6 +466,9 @@ 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):
logger.error("request contains invalid path: %r (not compliant to %r)", path, self._validate_path_value)
return response(*httputils.BAD_REQUEST)
# Get function corresponding to method # Get function corresponding to method
function = getattr(self, "do_%s" % request_method, None) function = getattr(self, "do_%s" % request_method, None)
@@ -496,7 +499,11 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
self.configuration, environ, base64.b64decode( self.configuration, environ, base64.b64decode(
authorization.encode("ascii"))).split(":", 1) authorization.encode("ascii"))).split(":", 1)
(user, info) = self._auth.login(login, password, context) or ("", "") if login else ("", "") if login and not self._check_user_format(login):
info = "not compliant to %r" % self._validate_user_value
user = ""
else:
(user, info) = self._auth.login(login, password, context) or ("", "") if login else ("", "")
if self.configuration.get("auth", "type") == "ldap": if self.configuration.get("auth", "type") == "ldap":
try: try:
logger.debug("Groups received from LDAP: %r", ",".join(self._auth._ldap_groups)) logger.debug("Groups received from LDAP: %r", ",".join(self._auth._ldap_groups))

View File

@@ -17,7 +17,9 @@
import io import io
import logging import logging
import re
import sys import sys
import unicodedata
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from typing import Optional, Union from typing import Optional, Union
@@ -30,6 +32,18 @@ from radicale.rights import intersect
import defusedxml.ElementTree as DefusedET # isort:skip import defusedxml.ElementTree as DefusedET # isort:skip
sys.modules["xml.etree"].ElementTree = ET # type:ignore[attr-defined] sys.modules["xml.etree"].ElementTree = ET # type:ignore[attr-defined]
USER_PATTERN_STRICT: str = "a-zA-Z0-9@\\.\\-_"
PATH_PATTERN_STRICT: str = USER_PATTERN_STRICT + "\\/" # / as separator
USER_PATTERN_STRICT_RE: str = "^[" + USER_PATTERN_STRICT + "]+$"
PATH_PATTERN_STRICT_RE: str = "^[" + PATH_PATTERN_STRICT + "]+$"
USER_BLACKLIST_MINIMAL: list = [":", "'", '"', '*', '?']
PATH_BLACKLIST_MINIMAL: list = USER_BLACKLIST_MINIMAL
USER_WHITELIST_UNICODE: list = ["-", ".", "@"] # from USER_PATTERN_STRICT
PATH_WHITELIST_UNICODE: list = ["-", ".", "@", "/"] # from PATH_PATTERN_STRICT
class ApplicationBase: class ApplicationBase:
@@ -103,6 +117,52 @@ 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 = (validation_type == "unicodeletter")
logger.trace("_check_format investigate %r (validation_type=%r check_minimal=%s check_unicode=%s)", string, validation_type, check_minimal, check_unicode)
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":
# Unicode: control
return False
if check_minimal:
if c in blacklist_minimal:
logger.trace("_check_format found %r", c)
return False
elif check_unicode:
if c not in whitelist_unicode:
if unicodedata.category(c)[0] != "L":
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

@@ -82,6 +82,9 @@ 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):
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 + "/"): if not (to_path + "/").startswith(base_prefix + "/"):
logger.warning("Destination %r from MOVE request on %r doesn't " logger.warning("Destination %r from MOVE request on %r doesn't "
"start with base prefix", to_path, path) "start with base prefix", to_path, path)

View File

@@ -122,10 +122,6 @@ API_TYPES_V1: dict[str, type] = {
TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}" TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}"
PATH_PATTERN: str = "([a-zA-Z0-9/.\\-@]+)" # TODO: extend or find better source
USER_PATTERN: str = "([a-zA-Z0-9@.]+)" # TODO: extend or find better source
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname") OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname")
CONVERSIONS_WHITELIST: Sequence[str] = ("bday", "none") CONVERSIONS_WHITELIST: Sequence[str] = ("bday", "none")
@@ -727,20 +723,20 @@ 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 re.search('^' + PATH_PATTERN + '$', request_data[key]): if not self._check_path_format(request_data[key]):
logger.warning(api_info + ": unsupported " + key) 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 re.search('^' + PATH_PATTERN + '$', request_data[key]): if not self._check_path_format(request_data[key]):
logger.warning(api_info + ": unsupported " + key) 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 re.search('^' + USER_PATTERN + '$', request_data[key]): if not self._check_user_format(request_data[key]):
logger.warning(api_info + ": unsupported " + key) 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")
# check for optional parameters # check for optional parameters