new option for checking path/user value

This commit is contained in:
Peter Bieringer
2026-04-21 18:51:58 +02:00
parent e279e4f803
commit af5121ab34
6 changed files with 66 additions and 1 deletions

View File

@@ -1617,6 +1617,33 @@ Strict preconditions check on PUT in case item already exists [RFC6352#9.2](http
Default: `False` 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 ##### hook
Command that is run after changes to storage. See the Command that is run after changes to storage. See the

8
config
View File

@@ -58,6 +58,14 @@
# script name to strip from URI if called by reverse proxy # script name to strip from URI if called by reverse proxy
#script_name = (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME) #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] [encoding]

View File

@@ -86,6 +86,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
_profiling_per_request: bool = False _profiling_per_request: bool = False
_profiling_per_request_method: bool = False _profiling_per_request_method: bool = False
_limit_content: int _limit_content: int
_validate_user_value: str
_validate_path_value: str
profiler_per_request_method: dict[str, cProfile.Profile] = {} profiler_per_request_method: dict[str, cProfile.Profile] = {}
profiler_per_request_method_counter: dict[str, int] = {} profiler_per_request_method_counter: dict[str, int] = {}
profiler_per_request_method_starttime: datetime.datetime profiler_per_request_method_starttime: datetime.datetime
@@ -158,6 +160,11 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
self._extra_headers[key] = configuration.get("headers", key) self._extra_headers[key] = configuration.get("headers", key)
self._strict_preconditions = configuration.get("storage", "strict_preconditions") self._strict_preconditions = configuration.get("storage", "strict_preconditions")
logger.info("strict preconditions check: %s", self._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 # Profiling options
self._profiling = configuration.get("logging", "profiling") self._profiling = configuration.get("logging", "profiling")
self._profiling_per_request_min_duration = configuration.get("logging", "profiling_per_request_min_duration") self._profiling_per_request_min_duration = configuration.get("logging", "profiling_per_request_min_duration")

View File

@@ -44,6 +44,8 @@ class ApplicationBase:
_permit_delete_collection: bool _permit_delete_collection: bool
_permit_overwrite_collection: bool _permit_overwrite_collection: bool
_strict_preconditions: bool _strict_preconditions: bool
_validate_user_value: str
_validate_path_format: str
_hook: hook.BaseHook _hook: hook.BaseHook
def __init__(self, configuration: config.Configuration) -> None: 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._response_content_on_debug = configuration.get("logging", "response_content_on_debug")
self._request_content_on_debug = configuration.get("logging", "request_content_on_debug") self._request_content_on_debug = configuration.get("logging", "request_content_on_debug")
self._limit_content = configuration.get("logging", "limit_content") 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) self._hook = hook.load(configuration)
def _read_xml_request_body(self, environ: types.WSGIEnviron def _read_xml_request_body(self, environ: types.WSGIEnviron

View File

@@ -48,6 +48,8 @@ DEFAULT_CONFIG_PATH: str = os.pathsep.join([
PROFILING: Sequence[str] = ("per_request", "per_request_method", "none") PROFILING: Sequence[str] = ("per_request", "per_request_method", "none")
VALIDATE_TYPES: Sequence[str] = ("none", "minimal", "unicodeletter", "strict")
def positive_int(value: Any) -> int: def positive_int(value: Any) -> int:
value = int(value) value = int(value)
@@ -84,6 +86,12 @@ def logging_level(value: Any) -> str:
return value 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: def profiling(value: Any) -> str:
if value not in PROFILING: if value not in PROFILING:
raise ValueError("unsupported profiling: %r" % value) raise ValueError("unsupported profiling: %r" % value)
@@ -221,6 +229,14 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
"value": "", "value": "",
"help": "script name to strip from URI if called by reverse proxy (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME)", "help": "script name to strip from URI if called by reverse proxy (default taken from HTTP_X_SCRIPT_NAME or SCRIPT_NAME)",
"type": str}), "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", { ("_internal_server", {
"value": "False", "value": "False",
"help": "the internal server is used", "help": "the internal server is used",

View File

@@ -29,6 +29,7 @@ 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")
@@ -135,7 +136,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: class BaseSharing(ApplicationBase):
_storage: storage.BaseStorage _storage: storage.BaseStorage
_rights: rights.BaseRights _rights: rights.BaseRights
@@ -157,6 +158,8 @@ class BaseSharing:
self._rights = rights.load(configuration) self._rights = rights.load(configuration)
self._storage = storage.load(configuration) self._storage = storage.load(configuration)
self._auth_delay = configuration.get("auth", "delay") 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 # Sharing
self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map") self.sharing_collection_by_map = configuration.get("sharing", "collection_by_map")
self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token") self.sharing_collection_by_token = configuration.get("sharing", "collection_by_token")