1
0
Fork 0
mirror of https://github.com/Kozea/Radicale.git synced 2025-08-01 18:18:31 +00:00

support max_occurence anti-DoS in xml_report for vevent expansion

Signed-off-by: David Greaves <david@dgreaves.com>
This commit is contained in:
David Greaves 2025-07-07 21:02:43 +01:00
parent a26bfaa08a
commit fbc3abc48d

View file

@ -145,7 +145,8 @@ def free_busy_report(base_prefix: str, path: str, xml_request: Optional[ET.Eleme
def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element], def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
collection: storage.BaseCollection, encoding: str, collection: storage.BaseCollection, encoding: str,
unlock_storage_fn: Callable[[], None] unlock_storage_fn: Callable[[], None],
max_occurrence: int = 0,
) -> Tuple[int, ET.Element]: ) -> Tuple[int, ET.Element]:
"""Read and answer REPORT requests that return XML. """Read and answer REPORT requests that return XML.
@ -244,6 +245,7 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
# !!! Don't access storage after this !!! # !!! Don't access storage after this !!!
unlock_storage_fn() unlock_storage_fn()
n_vevents = 0
while retrieved_items: while retrieved_items:
# ``item.vobject_item`` might be accessed during filtering. # ``item.vobject_item`` might be accessed during filtering.
# Don't keep reference to ``item``, because VObject requires a lot of # Don't keep reference to ``item``, because VObject requires a lot of
@ -298,15 +300,21 @@ def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
if time_range_element is not None: if time_range_element is not None:
time_range_start, time_range_end = radicale_filter.parse_time_range(time_range_element) time_range_start, time_range_end = radicale_filter.parse_time_range(time_range_element)
expanded_element = _expand( (expanded_element, n_vev) = _expand(
element=element, item=copy.copy(item), element=element, item=copy.copy(item),
start=start, end=end, start=start, end=end,
time_range_start=time_range_start, time_range_end=time_range_end, time_range_start=time_range_start, time_range_end=time_range_end,
max_occurrence=max_occurrence,
) )
n_vevents += n_vev
found_props.append(expanded_element) found_props.append(expanded_element)
else: else:
found_props.append(element) found_props.append(element)
n_vevents += 1
# Avoid DoS with too many events
if max_occurrence and n_vevents > max_occurrence:
raise ValueError("REPORT occurrences limit of {} hit"
.format(max_occurrence))
else: else:
not_found_props.append(element) not_found_props.append(element)
@ -327,7 +335,8 @@ def _expand(
end: datetime.datetime, end: datetime.datetime,
time_range_start: Optional[datetime.datetime] = None, time_range_start: Optional[datetime.datetime] = None,
time_range_end: Optional[datetime.datetime] = None, time_range_end: Optional[datetime.datetime] = None,
) -> ET.Element: max_occurrence: int = 0,
) -> Tuple[ET.Element, int]:
vevent_component: vobject.base.Component = copy.copy(item.vobject_item) vevent_component: vobject.base.Component = copy.copy(item.vobject_item)
logger.info("Expanding event %s", item.href) logger.info("Expanding event %s", item.href)
@ -401,7 +410,13 @@ def _expand(
# that event should be included as it is still ongoing. If no # that event should be included as it is still ongoing. If no
# extra point is generated then it was a no-op. # extra point is generated then it was a no-op.
rstart = start - duration if duration and duration.total_seconds() > 0 else start rstart = start - duration if duration and duration.total_seconds() > 0 else start
recurrences = rruleset.between(rstart, end, inc=True) recurrences = rruleset.between(rstart, end, inc=True, count=max_occurrence)
if max_occurrence and len(recurrences) >= max_occurrence:
# this shouldn't be > and if it's == then assume a limit
# was hit and ignore that maybe some would be filtered out
# by EXDATE etc. This is anti-DoS, not precise limits
raise ValueError("REPORT occurrences limit of {} hit"
.format(max_occurrence))
_strip_component(vevent_component) _strip_component(vevent_component)
_strip_single_event(vevent_recurrence, dt_format) _strip_single_event(vevent_recurrence, dt_format)
@ -500,14 +515,14 @@ def _expand(
if not filtered_vevents: if not filtered_vevents:
element.text = "" element.text = ""
return element return element, 0
else: else:
vevent_component.vevent_list = filtered_vevents vevent_component.vevent_list = filtered_vevents
logger.debug("lbt: vevent_component %s", vevent_component) logger.debug("lbt: vevent_component %s", vevent_component)
element.text = vevent_component.serialize() element.text = vevent_component.serialize()
return element return element, len(filtered_vevents)
def _convert_timezone(vevent: vobject.icalendar.RecurringComponent, def _convert_timezone(vevent: vobject.icalendar.RecurringComponent,
@ -744,9 +759,9 @@ class ApplicationPartReport(ApplicationBase):
assert item.collection is not None assert item.collection is not None
collection = item.collection collection = item.collection
max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence")
if xml_content is not None and \ if xml_content is not None and \
xml_content.tag == xmlutils.make_clark("C:free-busy-query"): xml_content.tag == xmlutils.make_clark("C:free-busy-query"):
max_occurrence = self.configuration.get("reporting", "max_freebusy_occurrence")
try: try:
status, body = free_busy_report( status, body = free_busy_report(
base_prefix, path, xml_content, collection, self._encoding, base_prefix, path, xml_content, collection, self._encoding,
@ -761,7 +776,7 @@ class ApplicationPartReport(ApplicationBase):
try: try:
status, xml_answer = xml_report( status, xml_answer = xml_report(
base_prefix, path, xml_content, collection, self._encoding, base_prefix, path, xml_content, collection, self._encoding,
lock_stack.close) lock_stack.close, max_occurrence)
except ValueError as e: except ValueError as e:
logger.warning( logger.warning(
"Bad REPORT request on %r: %s", path, e, exc_info=True) "Bad REPORT request on %r: %s", path, e, exc_info=True)