diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md index 6f1a570b..3d6650ed 100644 --- a/DOCUMENTATION.md +++ b/DOCUMENTATION.md @@ -1617,6 +1617,33 @@ Strict preconditions check on PUT in case item already exists [RFC6352#9.2](http Default: `False` +##### validate_user_value + +_(>= 3.7.2)_ + +Validate user value content + +Available types are: +* `none` +* `minimal` (control and some special chars) +* `unicodeletter` (unicode letters) +* `strict` (reduced ASCII set) + +Default: `minimum` + +##### validate_path_type + +_(>= 3.7.2)_ + +Validate path value content + +* `none` +* `minimal` (control and some special chars) +* `unicodeletter` (unicode letters) +* `strict` (reduced ASCII set) + +Default: `minimum` + ##### hook Command that is run after changes to storage. See the diff --git a/config b/config index 00cb3383..024fd868 100644 --- a/config +++ b/config @@ -58,6 +58,14 @@ # script name to strip from URI if called by reverse proxy #script_name = (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME) +# validate user type +# Value: none|minimal|unicodeletter|strict +#validate_user_type = minimal + +# validate path value +# Value: none|minimal|unicodeletter|strict +#validate_path_type = minimal + [encoding] diff --git a/radicale/app/__init__.py b/radicale/app/__init__.py index 9956e023..71cd8f1c 100644 --- a/radicale/app/__init__.py +++ b/radicale/app/__init__.py @@ -86,6 +86,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead, _profiling_per_request: bool = False _profiling_per_request_method: bool = False _limit_content: int + _validate_user_value: str + _validate_path_value: str profiler_per_request_method: dict[str, cProfile.Profile] = {} profiler_per_request_method_counter: dict[str, int] = {} profiler_per_request_method_starttime: datetime.datetime @@ -158,6 +160,11 @@ class Application(ApplicationPartDelete, ApplicationPartHead, self._extra_headers[key] = configuration.get("headers", key) self._strict_preconditions = configuration.get("storage", "strict_preconditions") logger.info("strict preconditions check: %s", self._strict_preconditions) + # Format checks + self._validate_user_value = configuration.get("server", "validate_user_value") + self._validate_path_value = configuration.get("server", "validate_path_value") + logger.info("validate user value: %r", self._validate_user_value) + logger.info("validate path value: %r", self._validate_path_value) # Profiling options self._profiling = configuration.get("logging", "profiling") self._profiling_per_request_min_duration = configuration.get("logging", "profiling_per_request_min_duration") diff --git a/radicale/app/base.py b/radicale/app/base.py index cff58ec1..7d89758a 100644 --- a/radicale/app/base.py +++ b/radicale/app/base.py @@ -44,6 +44,8 @@ class ApplicationBase: _permit_delete_collection: bool _permit_overwrite_collection: bool _strict_preconditions: bool + _validate_user_value: str + _validate_path_format: str _hook: hook.BaseHook def __init__(self, configuration: config.Configuration) -> None: @@ -58,6 +60,8 @@ class ApplicationBase: self._response_content_on_debug = configuration.get("logging", "response_content_on_debug") self._request_content_on_debug = configuration.get("logging", "request_content_on_debug") self._limit_content = configuration.get("logging", "limit_content") + self._validate_user_value = configuration.get("server", "validate_user_value") + self._validate_path_value = configuration.get("server", "validate_path_value") self._hook = hook.load(configuration) def _read_xml_request_body(self, environ: types.WSGIEnviron diff --git a/radicale/config.py b/radicale/config.py index 581d4939..2de9c2da 100644 --- a/radicale/config.py +++ b/radicale/config.py @@ -48,6 +48,8 @@ DEFAULT_CONFIG_PATH: str = os.pathsep.join([ PROFILING: Sequence[str] = ("per_request", "per_request_method", "none") +VALIDATE_TYPES: Sequence[str] = ("none", "minimal", "unicodeletter", "strict") + def positive_int(value: Any) -> int: value = int(value) @@ -84,6 +86,12 @@ def logging_level(value: Any) -> str: return value +def validate_types(value: Any) -> str: + if value not in VALIDATE_TYPES: + raise ValueError("unsupported validation type: %r" % value) + return value + + def profiling(value: Any) -> str: if value not in PROFILING: raise ValueError("unsupported profiling: %r" % value) @@ -221,6 +229,14 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([ "value": "", "help": "script name to strip from URI if called by reverse proxy (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME)", "type": str}), + ("validate_user_value", { + "value": "minimal", + "help": "validate user value (" + "|".join(VALIDATE_TYPES) + ")", + "type": validate_types}), + ("validate_path_value", { + "value": "minimal", + "help": "validate path value (" + "|".join(VALIDATE_TYPES) + ")", + "type": validate_types}), ("_internal_server", { "value": "False", "help": "the internal server is used", diff --git a/radicale/sharing/__init__.py b/radicale/sharing/__init__.py index 41b8067b..01f7ff83 100644 --- a/radicale/sharing/__init__.py +++ b/radicale/sharing/__init__.py @@ -29,6 +29,7 @@ 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") @@ -135,7 +136,7 @@ def load(configuration: "config.Configuration") -> "BaseSharing": return utils.load_plugin(INTERNAL_TYPES, "sharing", "Sharing", BaseSharing, configuration) -class BaseSharing: +class BaseSharing(ApplicationBase): _storage: storage.BaseStorage _rights: rights.BaseRights @@ -157,6 +158,8 @@ class BaseSharing: self._rights = rights.load(configuration) self._storage = storage.load(configuration) self._auth_delay = configuration.get("auth", "delay") + self._validate_user_value = configuration.get("server", "validate_user_value") + self._validate_path_value = configuration.get("server", "validate_path_value") # Sharing self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map") self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token")