sharing/bday/age_max: input validation

This commit is contained in:
Peter Bieringer
2026-05-30 16:22:02 +02:00
parent 71643860a4
commit ebb359c5cc
3 changed files with 76 additions and 10 deletions

View File

@@ -609,7 +609,7 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
("conversion_bday_age_max", { ("conversion_bday_age_max", {
"value": "99", "value": "99",
"help": "conversion bday age max", "help": "conversion bday age max",
"type": positive_int}), "type": sharing.check_bday_max_age}),
])), ])),
("hook", OrderedDict([ ("hook", OrderedDict([
("type", { ("type", {

View File

@@ -24,7 +24,7 @@ import uuid
from csv import DictWriter from csv import DictWriter
from datetime import datetime from datetime import datetime
from http import client from http import client
from typing import Sequence, Union from typing import Any, Sequence, Union
from urllib.parse import parse_qs from urllib.parse import parse_qs
from radicale import (config, httputils, pathutils, rights, storage, types, from radicale import (config, httputils, pathutils, rights, storage, types,
@@ -123,14 +123,24 @@ TOKEN_PATTERN_V1: str = "v1/[a-zA-Z0-9_\\-]{44}"
OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname", "ICAL:calendar-order") OVERLAY_PROPERTIES_WHITELIST: Sequence[str] = ("C:calendar-description", "ICAL:calendar-color", "CR:addressbook-description", "INF:addressbook-color", "D:displayname", "ICAL:calendar-order")
SHARING_BDAY_AGE_MAX: int = 199 # maximum age to prevent unexpected DoS by config
def check_bday_max_age(data: Any) -> int:
value = int(data)
if value < 0:
raise ValueError("value is negative: %d" % value)
if value > SHARING_BDAY_AGE_MAX:
raise ValueError("value exceeds maximum (%d): %d" % (SHARING_BDAY_AGE_MAX, value))
return value
ACTIONS_WHITELIST: dict = { ACTIONS_WHITELIST: dict = {
'template': { 'config': {
'conversion_bday_summary_template': str, 'conversion_bday_summary_template': str,
'conversion_bday_description_template': str, 'conversion_bday_description_template': str,
'conversion_bday_alarm_trigger_template': str, 'conversion_bday_alarm_trigger_template': str,
}, 'conversion_bday_age_max': check_bday_max_age,
'limit': {
'conversion_bday_age_max': "positive_int",
}, },
} }
@@ -862,9 +872,12 @@ class BaseSharing:
if level1 in ACTIONS_WHITELIST: if level1 in ACTIONS_WHITELIST:
for level2 in request_data['Actions'][level1]: for level2 in request_data['Actions'][level1]:
if level2 in ACTIONS_WHITELIST[level1]: if level2 in ACTIONS_WHITELIST[level1]:
if ACTIONS_WHITELIST[level1][level2] == "positive_int": logger.trace(api_info + ": Actions validation: type='%r'", type(ACTIONS_WHITELIST[level1][level2]))
if int(request_data['Actions'][level1][level2]) < 0: if callable(ACTIONS_WHITELIST[level1][level2]):
hint = "'" + level1 + "': {'" + level2 + "'} is negative" try:
value = ACTIONS_WHITELIST[level1][level2](request_data['Actions'][level1][level2])
except ValueError:
hint = "'" + level1 + "': {'" + level2 + "'} is out-of-range"
valid = False valid = False
break break
pass pass

View File

@@ -5131,6 +5131,26 @@ permissions: RrWw""")
"conversion_bday_description_template": "BDAY={year}-{month}-{day}", "conversion_bday_description_template": "BDAY={year}-{month}-{day}",
}}) }})
logging.info("\n*** configuration test: conversion_bday_age_max < 0")
try:
self.configure({"sharing": {
"conversion_bday_age_max": -1,
}})
except RuntimeError:
pass
else:
raise
logging.info("\n*** configuration test: conversion_bday_age_max > MAX")
try:
self.configure({"sharing": {
"conversion_bday_age_max": 200,
}})
except RuntimeError:
pass
else:
raise
# verify content as user # verify content as user
logging.info("\n*** GET collection user format:default -> ok") logging.info("\n*** GET collection user format:default -> ok")
_, headers, answer = self.request("GET", path_shared_2, login="user:userpw") _, headers, answer = self.request("GET", path_shared_2, login="user:userpw")
@@ -5217,11 +5237,44 @@ permissions: RrWw""")
json_dict['User'] = "user" json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"limit": { json_dict['Actions'] = {"config": {
"conversion_bday_age_max": -1, "conversion_bday_age_max": -1,
}} }}
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict) _, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
# update template with invalid data test
logging.info("\n*** update map(bday) user/owner:r -> age_max exceeds MAX")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_age_max": (sharing.SHARING_BDAY_AGE_MAX + 1),
}}
_, headers, answer = self._sharing_api_json("map", "update", check=400, login="owner:ownerpw", json_dict=json_dict)
# update template with valid data test
logging.info("\n*** update map(bday) user/owner:r -> age_max ok")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_age_max": 0,
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
# update template with valid data test
logging.info("\n*** update map(bday) user/owner:r -> age_max < MAX")
json_dict = {}
json_dict['User'] = "user"
json_dict['PathMapped'] = path_mapped
json_dict['PathOrToken'] = path_shared_r
json_dict['Actions'] = {"config": {
"conversion_bday_age_max": sharing.SHARING_BDAY_AGE_MAX,
}}
_, headers, answer = self._sharing_api_json("map", "update", check=200, login="owner:ownerpw", json_dict=json_dict)
logging.info("\n*** update map(bday) user/owner:r -> unsupported level 1") logging.info("\n*** update map(bday) user/owner:r -> unsupported level 1")
json_dict = {} json_dict = {}
json_dict['User'] = "user" json_dict['User'] = "user"