max_vevent_rrule_occurrence: rename option
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
* Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions
|
* Fix: sharing/proppatch: reject in case of write-access but 'p' is in permissions
|
||||||
* Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync")
|
* Fix: sharing/by-map: catch collection path without trailing / (supporting "pimsync")
|
||||||
* Add: [report] max_expand_occurrence option to separate from max_freebusy_occurrence
|
* Add: [report] max_expand_occurrence option to separate from max_freebusy_occurrence
|
||||||
* Add: [system] max_vevent_rrule_entries option to catch DoS by problematic RRULE early enough
|
* Add: [system] max_vevent_rrule_occurrence option to catch DoS by problematic RRULE early enough (workaround for missing protection in current vobject version)
|
||||||
|
|
||||||
## 3.7.7
|
## 3.7.7
|
||||||
* Fix: web plugin helpers httputils.serve_resource/serve_folder ignored their mimetypes and fallback_mimetype parameters and always used the built-in mapping, so custom web plugins could not serve additional file types with a correct Content-Type
|
* Fix: web plugin helpers httputils.serve_resource/serve_folder ignored their mimetypes and fallback_mimetype parameters and always used the built-in mapping, so custom web plugins could not serve additional file types with a correct Content-Type
|
||||||
|
|||||||
@@ -866,12 +866,12 @@ Limited to 80% of max_content_length to cover plain base64 encoded payload.
|
|||||||
|
|
||||||
Announced to clients requesting "max-resource-size" via PROPFIND.
|
Announced to clients requesting "max-resource-size" via PROPFIND.
|
||||||
|
|
||||||
##### max_vevent_rrule_entries
|
##### max_vevent_rrule_occurrence
|
||||||
|
|
||||||
_(>= 3.7.8)_
|
_(>= 3.7.8)_
|
||||||
|
|
||||||
The maximum of generated entries of an rrule of an vevent.
|
The maximum of occurrence by an rrule of a vevent.
|
||||||
Large time frames of RRULE by COUNT or UNTIL could
|
Large time frames defined in RRULE by COUNT or UNTIL could
|
||||||
generate a lot of occurrences based on the time frame supplied. This
|
generate a lot of occurrences based on the time frame supplied. This
|
||||||
setting limits the lookup to prevent potential denial of service
|
setting limits the lookup to prevent potential denial of service
|
||||||
attacks on large time frames. If the limit is reached, an HTTP error
|
attacks on large time frames. If the limit is reached, an HTTP error
|
||||||
|
|||||||
4
config
4
config
@@ -33,8 +33,8 @@
|
|||||||
# Announced to clients requesting "max-resource-size" via PROPFIND
|
# Announced to clients requesting "max-resource-size" via PROPFIND
|
||||||
#max_resource_size = 10000000
|
#max_resource_size = 10000000
|
||||||
|
|
||||||
# Max entries of a RRULE, limit the number to prevent DoS attacks.
|
# Max occurrence by an RRULE, limit the number to prevent DoS attacks.
|
||||||
#max_vevent_rrule_entries = 10000
|
#max_vevent_rrule_occurrence = 10000
|
||||||
|
|
||||||
# Socket timeout (seconds)
|
# Socket timeout (seconds)
|
||||||
#timeout = 30
|
#timeout = 30
|
||||||
|
|||||||
@@ -200,10 +200,10 @@ def run() -> None:
|
|||||||
|
|
||||||
if args_ns.verify_item:
|
if args_ns.verify_item:
|
||||||
encoding = configuration.get("encoding", "stock")
|
encoding = configuration.get("encoding", "stock")
|
||||||
max_vevent_rrule_entries = configuration.get("server", "max_vevent_rrule_entries")
|
max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence")
|
||||||
logger.info("Item verification start using 'stock' encoding: %s", encoding)
|
logger.info("Item verification start using 'stock' encoding: %s", encoding)
|
||||||
try:
|
try:
|
||||||
if not item.verify(args_ns.verify_item[0], encoding, max_vevent_rrule_entries):
|
if not item.verify(args_ns.verify_item[0], encoding, max_vevent_rrule_occurrence):
|
||||||
logger.critical("Item verification failed")
|
logger.critical("Item verification failed")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||||||
_internal_server: bool
|
_internal_server: bool
|
||||||
_max_content_length: int
|
_max_content_length: int
|
||||||
_max_resource_size: int
|
_max_resource_size: int
|
||||||
_max_vevent_rrule_entries: int
|
_max_vevent_rrule_occurrence: int
|
||||||
_auth_realm: str
|
_auth_realm: str
|
||||||
_auth_type: str
|
_auth_type: str
|
||||||
_web_type: str
|
_web_type: str
|
||||||
@@ -121,8 +121,8 @@ class Application(ApplicationPartDelete, ApplicationPartHead,
|
|||||||
self._max_resource_size = max_resource_size_limited
|
self._max_resource_size = max_resource_size_limited
|
||||||
else:
|
else:
|
||||||
logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True))
|
logger.info("max_resource_size set to: %d bytes (%sbytes)", self._max_resource_size, utils.format_unit(self._max_resource_size, binary=True))
|
||||||
self._max_vevent_rrule_entries = configuration.get("server", "max_vevent_rrule_entries")
|
self._max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence")
|
||||||
logger.info("max_vevent_rrule_entries set to: %d", self._max_vevent_rrule_entries)
|
logger.info("max_vevent_rrule_occurrence set to: %d", self._max_vevent_rrule_occurrence)
|
||||||
self._bad_put_request_content = configuration.get("logging", "bad_put_request_content")
|
self._bad_put_request_content = configuration.get("logging", "bad_put_request_content")
|
||||||
logger.info("log bad put request content: %s", self._bad_put_request_content)
|
logger.info("log bad put request content: %s", self._bad_put_request_content)
|
||||||
self._request_header_on_debug = configuration.get("logging", "request_header_on_debug")
|
self._request_header_on_debug = configuration.get("logging", "request_header_on_debug")
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ class ApplicationBase:
|
|||||||
_sharing: sharing.BaseSharing
|
_sharing: sharing.BaseSharing
|
||||||
_encoding: str
|
_encoding: str
|
||||||
_max_resource_size: int
|
_max_resource_size: int
|
||||||
_max_vevent_rrule_entries: int
|
_max_vevent_rrule_occurrence: int
|
||||||
_permit_delete_collection: bool
|
_permit_delete_collection: bool
|
||||||
_permit_overwrite_collection: bool
|
_permit_overwrite_collection: bool
|
||||||
_strict_preconditions: bool
|
_strict_preconditions: bool
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ PRODID = u"-//Radicale//NONSGML Version " + utils.package_version("radicale") +
|
|||||||
|
|
||||||
def prepare(vobject_items: List[vobject.base.Component], path: str,
|
def prepare(vobject_items: List[vobject.base.Component], path: str,
|
||||||
content_type: str, permission: bool, parent_permission: bool, max_resource_size: int,
|
content_type: str, permission: bool, parent_permission: bool, max_resource_size: int,
|
||||||
max_vevent_rrule_entries: int,
|
max_vevent_rrule_occurrence: int,
|
||||||
tag: Optional[str] = None,
|
tag: Optional[str] = None,
|
||||||
write_whole_collection: Optional[bool] = None) -> Tuple[
|
write_whole_collection: Optional[bool] = None) -> Tuple[
|
||||||
Iterator[radicale_item.Item], # items
|
Iterator[radicale_item.Item], # items
|
||||||
@@ -75,7 +75,7 @@ def prepare(vobject_items: List[vobject.base.Component], path: str,
|
|||||||
if tag and write_whole_collection is not None:
|
if tag and write_whole_collection is not None:
|
||||||
radicale_item.check_and_sanitize_items(
|
radicale_item.check_and_sanitize_items(
|
||||||
vobject_items,
|
vobject_items,
|
||||||
max_vevent_rrule_entries=max_vevent_rrule_entries,
|
max_vevent_rrule_occurrence=max_vevent_rrule_occurrence,
|
||||||
is_collection=write_whole_collection, tag=tag)
|
is_collection=write_whole_collection, tag=tag)
|
||||||
if write_whole_collection and tag == "VCALENDAR":
|
if write_whole_collection and tag == "VCALENDAR":
|
||||||
vobject_components: List[vobject.base.Component] = []
|
vobject_components: List[vobject.base.Component] = []
|
||||||
@@ -228,7 +228,7 @@ class ApplicationPartPut(ApplicationBase):
|
|||||||
bool(rights.intersect(access.permissions, "Ww")),
|
bool(rights.intersect(access.permissions, "Ww")),
|
||||||
bool(rights.intersect(access.parent_permissions, "w")),
|
bool(rights.intersect(access.parent_permissions, "w")),
|
||||||
self._max_resource_size,
|
self._max_resource_size,
|
||||||
self._max_vevent_rrule_entries,
|
self._max_vevent_rrule_occurrence,
|
||||||
)
|
)
|
||||||
|
|
||||||
with self._storage.acquire_lock("w", user, path=path, request="PUT"):
|
with self._storage.acquire_lock("w", user, path=path, request="PUT"):
|
||||||
@@ -294,7 +294,7 @@ class ApplicationPartPut(ApplicationBase):
|
|||||||
bool(rights.intersect(access.permissions, "Ww")),
|
bool(rights.intersect(access.permissions, "Ww")),
|
||||||
bool(rights.intersect(access.parent_permissions, "w")),
|
bool(rights.intersect(access.parent_permissions, "w")),
|
||||||
self._max_resource_size,
|
self._max_resource_size,
|
||||||
self._max_vevent_rrule_entries,
|
self._max_vevent_rrule_occurrence,
|
||||||
tag, write_whole_collection)
|
tag, write_whole_collection)
|
||||||
props = prepared_props
|
props = prepared_props
|
||||||
if prepared_exc_info:
|
if prepared_exc_info:
|
||||||
|
|||||||
@@ -254,9 +254,9 @@ DEFAULT_CONFIG_SCHEMA: types.CONFIG_SCHEMA = OrderedDict([
|
|||||||
"value": "10000000",
|
"value": "10000000",
|
||||||
"help": "maximum size of resource (default: 10 Mbyte)",
|
"help": "maximum size of resource (default: 10 Mbyte)",
|
||||||
"type": positive_int}),
|
"type": positive_int}),
|
||||||
("max_vevent_rrule_entries", {
|
("max_vevent_rrule_occurrence", {
|
||||||
"value": "10000",
|
"value": "10000",
|
||||||
"help": "maximum of RRULE entries (default: 10000)",
|
"help": "maximum occurrence by an RRULE (default: 10000)",
|
||||||
"type": positive_int}),
|
"type": positive_int}),
|
||||||
("timeout", {
|
("timeout", {
|
||||||
"value": "30",
|
"value": "30",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def predict_tag_of_whole_collection(
|
|||||||
|
|
||||||
def check_and_sanitize_items(
|
def check_and_sanitize_items(
|
||||||
vobject_items: List[vobject.base.Component],
|
vobject_items: List[vobject.base.Component],
|
||||||
max_vevent_rrule_entries: int,
|
max_vevent_rrule_occurrence: int,
|
||||||
is_collection: bool = False, tag: str = "") -> None:
|
is_collection: bool = False, tag: str = "") -> None:
|
||||||
"""Check vobject items for common errors and add missing UIDs.
|
"""Check vobject items for common errors and add missing UIDs.
|
||||||
|
|
||||||
@@ -417,7 +417,7 @@ def find_time_range(vobject_item: vobject.base.Component, tag: str
|
|||||||
return math.floor(start.timestamp()), math.ceil(end.timestamp())
|
return math.floor(start.timestamp()), math.ceil(end.timestamp())
|
||||||
|
|
||||||
|
|
||||||
def verify(file: str, encoding: str, max_vevent_rrule_entries: int):
|
def verify(file: str, encoding: str, max_vevent_rrule_occurrence: int):
|
||||||
logger.info("Verifying item: %s", file)
|
logger.info("Verifying item: %s", file)
|
||||||
with open(file, "rb") as f:
|
with open(file, "rb") as f:
|
||||||
content_raw = f.read()
|
content_raw = f.read()
|
||||||
@@ -437,7 +437,7 @@ def verify(file: str, encoding: str, max_vevent_rrule_entries: int):
|
|||||||
try:
|
try:
|
||||||
tag = radicale_item.predict_tag_of_whole_collection(vobject_items)
|
tag = radicale_item.predict_tag_of_whole_collection(vobject_items)
|
||||||
if tag is not None:
|
if tag is not None:
|
||||||
radicale_item.check_and_sanitize_items(vobject_items, tag=tag, max_vevent_rrule_entries=max_vevent_rrule_entries)
|
radicale_item.check_and_sanitize_items(vobject_items, tag=tag, max_vevent_rrule_occurrence=max_vevent_rrule_occurrence)
|
||||||
else:
|
else:
|
||||||
raise ValueError("collection tag cannot be predicted")
|
raise ValueError("collection tag cannot be predicted")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ class StorageBase(storage.BaseStorage):
|
|||||||
"logging", "storage_cache_actions_on_debug")
|
"logging", "storage_cache_actions_on_debug")
|
||||||
self._max_resource_size = configuration.get(
|
self._max_resource_size = configuration.get(
|
||||||
"server", "max_resource_size")
|
"server", "max_resource_size")
|
||||||
self._max_vevent_rrule_entries = configuration.get("server", "max_vevent_rrule_entries")
|
self._max_vevent_rrule_occurrence = configuration.get("server", "max_vevent_rrule_occurrence")
|
||||||
|
|
||||||
def _get_collection_root_folder(self) -> str:
|
def _get_collection_root_folder(self) -> str:
|
||||||
return os.path.join(self._filesystem_folder, "collection-root")
|
return os.path.join(self._filesystem_folder, "collection-root")
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class CollectionPartGet(CollectionPartCache, CollectionPartLock,
|
|||||||
vobject_items = radicale_item.read_components(
|
vobject_items = radicale_item.read_components(
|
||||||
raw_text.decode(self._encoding))
|
raw_text.decode(self._encoding))
|
||||||
radicale_item.check_and_sanitize_items(
|
radicale_item.check_and_sanitize_items(
|
||||||
vobject_items, tag=self.tag, max_vevent_rrule_entries=self._storage._max_vevent_rrule_entries)
|
vobject_items, tag=self.tag, max_vevent_rrule_occurrence=self._storage._max_vevent_rrule_occurrence)
|
||||||
vobject_item, = vobject_items
|
vobject_item, = vobject_items
|
||||||
temp_item = radicale_item.Item(
|
temp_item = radicale_item.Item(
|
||||||
collection=self, vobject_item=vobject_item)
|
collection=self, vobject_item=vobject_item)
|
||||||
|
|||||||
@@ -330,35 +330,42 @@ permissions: RrWw""")
|
|||||||
|
|
||||||
def test_add_event_with_rrule_count_500_limit_100(self) -> None:
|
def test_add_event_with_rrule_count_500_limit_100(self) -> None:
|
||||||
"""Test event with RRULE COUNT=500 and limit 100."""
|
"""Test event with RRULE COUNT=500 and limit 100."""
|
||||||
self.configure({"server": {"max_vevent_rrule_entries": 100}})
|
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
event = get_file_content("event_full_day_rrule_count_500.ics")
|
event = get_file_content("event_full_day_rrule_count_500.ics")
|
||||||
self.put("/calendar.ics/event.ics", event, check=400)
|
self.put("/calendar.ics/event.ics", event, check=400)
|
||||||
|
|
||||||
def test_add_event_with_rrule_count_500_limit_600(self) -> None:
|
def test_add_event_with_rrule_count_500_limit_600(self) -> None:
|
||||||
"""Test event with RRULE COUNT=500 and limit 600."""
|
"""Test event with RRULE COUNT=500 and limit 600."""
|
||||||
self.configure({"server": {"max_vevent_rrule_entries": 600}})
|
self.configure({"server": {"max_vevent_rrule_occurrence": 600}})
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
event = get_file_content("event_full_day_rrule_count_500.ics")
|
event = get_file_content("event_full_day_rrule_count_500.ics")
|
||||||
self.put("/calendar.ics/event.ics", event)
|
self.put("/calendar.ics/event.ics", event)
|
||||||
|
|
||||||
|
def test_add_event_with_rrule_until_2y_limit_800(self) -> None:
|
||||||
|
"""Test event with RRULE UNTIL=+2y and limit 800."""
|
||||||
|
self.configure({"server": {"max_vevent_rrule_occurrence": 800}})
|
||||||
|
self.mkcalendar("/calendar.ics/")
|
||||||
|
event = get_file_content("event_full_day_rrule_until_2y.ics")
|
||||||
|
self.put("/calendar.ics/event.ics", event)
|
||||||
|
|
||||||
def test_add_event_with_rrule_until_2y_limit_100(self) -> None:
|
def test_add_event_with_rrule_until_2y_limit_100(self) -> None:
|
||||||
"""Test event with RRULE UNTIL=+2y and limit 100."""
|
"""Test event with RRULE UNTIL=+2y and limit 100."""
|
||||||
self.configure({"server": {"max_vevent_rrule_entries": 100}})
|
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
event = get_file_content("event_full_day_rrule_until_2y.ics")
|
event = get_file_content("event_full_day_rrule_until_2y.ics")
|
||||||
self.put("/calendar.ics/event.ics", event, check=400)
|
self.put("/calendar.ics/event.ics", event, check=400)
|
||||||
|
|
||||||
def test_add_event_with_rrule_until_50y_limit_100(self) -> None:
|
def test_add_event_with_rrule_until_50y_limit_100(self) -> None:
|
||||||
"""Test event with RRULE UNTIL=+50y and limit 100."""
|
"""Test event with RRULE UNTIL=+50y and limit 100."""
|
||||||
self.configure({"server": {"max_vevent_rrule_entries": 100}})
|
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
event = get_file_content("event_full_day_rrule_until_50y.ics")
|
event = get_file_content("event_full_day_rrule_until_50y.ics")
|
||||||
self.put("/calendar.ics/event.ics", event, check=400)
|
self.put("/calendar.ics/event.ics", event, check=400)
|
||||||
|
|
||||||
def test_add_event_with_rrule_until_5000y_limit_100(self) -> None:
|
def test_add_event_with_rrule_until_5000y_limit_100(self) -> None:
|
||||||
"""Test event with RRULE UNTIL=+5000y and limit 100."""
|
"""Test event with RRULE UNTIL=+5000y and limit 100."""
|
||||||
self.configure({"server": {"max_vevent_rrule_entries": 100}})
|
self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
|
||||||
self.mkcalendar("/calendar.ics/")
|
self.mkcalendar("/calendar.ics/")
|
||||||
event = get_file_content("event_full_day_rrule_until_5000y.ics")
|
event = get_file_content("event_full_day_rrule_until_5000y.ics")
|
||||||
self.put("/calendar.ics/event.ics", event, check=400)
|
self.put("/calendar.ics/event.ics", event, check=400)
|
||||||
|
|||||||
Reference in New Issue
Block a user