CVE-2025-59045
Description
Stalwart is a mail and collaboration server. Starting in version 0.12.0 and prior to version 0.13.3, a memory exhaustion vulnerability exists in Stalwart's CalDAV implementation that allows authenticated attackers to cause denial-of-service by triggering unbounded memory consumption through recurring event expansion. An authenticated attacker can crash the Stalwart server by creating recurring events with large payloads and triggering their expansion through CalDAV REPORT requests. A single malicious request expanding 300 events with 1000-character descriptions can consume up to 2 GB of memory. The vulnerability exists in the ArchivedCalendarEventData.expand function, which processes CalDAV REPORT requests with event expansion. When a client requests recurring events in their expanded form using the <C:expand> element, the server stores all expanded event instances in memory without enforcing size limits. Users should upgrade to Stalwart version 0.13.3 or later to receive a fix. If immediate upgrading is not possible, implement memory limits at the container/system level; monitor server memory usage for unusual spikes; consider rate limiting CalDAV REPORT requests; and restrict CalDAV access to trusted users only.
Affected products
1- Range: v0.10.0, v0.10.1, v0.10.2, …
Patches
115762fba2ba3CalDAV: Limit recurrence expansions in calendar reports
4 files changed · +49 −53
CHANGELOG.md+1 −0 modified@@ -27,6 +27,7 @@ If you are upgrading from v0.11.x or v0.12.x, this version includes **breaking c - IMAP: Add owner rights to ACL get responses. - IMAP: Do not auto-train Bayes when moving messages from Junk to Trash. - IMAP/ManageSieve: Increase maximum quoted argument size (fixes #2039). +- CalDAV: Limit recurrence expansions in calendar reports. - WebDAV: Do not fix percent encoding on WebDAV FS (closes #2036). ## [0.13.2] - 2025-07-28
crates/dav/src/calendar/query.rs+12 −2 modified@@ -420,7 +420,12 @@ impl CalendarQueryHandler { is_all || matches_one } - pub fn serialize_ical(&mut self, event: &ArchivedCalendarEvent, data: &CalendarData) -> String { + pub fn serialize_ical( + &mut self, + event: &ArchivedCalendarEvent, + data: &CalendarData, + instances_limit: &mut usize, + ) -> Option<String> { let mut out = String::with_capacity(event.size.to_native() as usize); let _v = [0.into()]; let mut component_iter: Iter<'_, rkyv::rend::u16_le> = _v.iter(); @@ -528,6 +533,11 @@ impl CalendarQueryHandler { && (!is_recurrent_or_override || expand.is_in_range(is_todo, event.start, event.end)) { + if *instances_limit > 0 { + *instances_limit -= 1; + } else { + return None; + } let _ = write!(&mut out, "BEGIN:{component_name}\r\n"); // Write DTSTART, DTEND and RECURRENCE-ID @@ -607,7 +617,7 @@ impl CalendarQueryHandler { } } - out + Some(out) } pub fn into_expanded_times(self) -> Vec<CalendarEvent<i64, i64>> {
crates/dav/src/common/propfind.rs+34 −49 modified@@ -296,6 +296,7 @@ impl PropFindRequestHandler for Server { ); let mut is_sync_limited = false; let mut is_propfind = false; + let mut ical_instances_limit = self.core.groupware.max_ical_instances; let paths = match std::mem::take(&mut query.resource) { DavQueryResource::Uri(resource) => { @@ -351,38 +352,6 @@ impl PropFindRequestHandler for Server { items } - /*DavQueryResource::Discovery { - parent_collection, - account_ids, - } => { - collection_container = parent_collection; - collection_children = collection_container.child_collection().unwrap(); - sync_collection = SyncCollection::from(collection_container); - - // Add container info - if !query.depth_no_root { - add_base_collection_response( - self, - &query.propfind, - parent_collection, - access_token, - &mut response, - ) - .await?; - } - - discover_root_paths( - self, - access_token, - collection_container, - sync_collection, - &query, - &mut data, - &mut response, - account_ids, - ) - .await? - }*/ DavQueryResource::None => unreachable!(), }; response.set_namespace(collection_container.namespace()); @@ -441,7 +410,7 @@ impl PropFindRequestHandler for Server { let view_as_id = access_token.primary_id(); let is_scheduling = collection_container == Collection::CalendarScheduling; - for item in paths { + 'outer: for item in paths { let account_id = item.account_id; let document_id = item.document_id; let collection = if item.is_container { @@ -981,20 +950,27 @@ impl PropFindRequestHandler for Server { CalDavProperty::CalendarData(data), ArchivedResource::CalendarEvent(event), ) => { - let ical = if calendar_filter.is_some() || !data.properties.is_empty() { - calendar_filter + if calendar_filter.is_some() || !data.properties.is_empty() { + if let Some(ical) = calendar_filter .get_or_insert_with(|| { CalendarQueryHandler::new(event.inner, None, Tz::UTC) }) - .serialize_ical(event.inner, data) + .serialize_ical(event.inner, data, &mut ical_instances_limit) + { + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::CData(ical), + )); + } else { + limit = 0; + break 'outer; + } } else { - event.inner.data.event.to_string() - }; - - fields.push(DavPropertyValue::new( - property.clone(), - DavValue::CData(ical), - )); + fields.push(DavPropertyValue::new( + property.clone(), + DavValue::CData(event.inner.data.event.to_string()), + )); + } } ( CalDavProperty::CalendarData(_), @@ -1092,12 +1068,21 @@ impl PropFindRequestHandler for Server { response.add_response( Response::new_status([query.uri], StatusCode::INSUFFICIENT_STORAGE) .with_error(BaseCondition::NumberOfMatchesWithinLimit) - .with_response_description(format!( - "The number of matches exceeds the limit of {}", - query - .limit - .unwrap_or(self.core.groupware.max_results as u32) - )), + .with_response_description(if ical_instances_limit > 0 { + format!( + "The number of matches exceeds the limit of {}", + query + .limit + .unwrap_or(self.core.groupware.max_results as u32) + ) + } else { + format!( + "The number of recurrence instances exceeds the limit of {}", + query + .limit + .unwrap_or(self.core.groupware.max_ical_instances as u32) + ) + }), ); }
.github/ISSUE_TEMPLATE/bug_report.yml+2 −2 modified@@ -1,12 +1,12 @@ -name: I am absolutely certain I found a bug +name: I found a bug description: Most reported issues turn out to be configuration problems rather than actual bugs. If you are not 100% certain this is a bug, please start a new discussion instead. title: "🐛: " labels: ["bug"] body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! Use this form only for reporting bugs. If you have a question or problem, please use the [Q&A discussion](https://github.com/stalwartlabs/stalwart/discussions/new?category=q-a). + Thanks for taking the time to fill out this bug report! Most reported issues turn out to be configuration problems rather than actual bugs. If you are not 100% certain this is a bug, please start a new [discussion](https://github.com/stalwartlabs/stalwart/discussions/new?category=q-a) instead. - type: textarea id: what-happened attributes:
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/stalwartlabs/stalwart/blob/main/CHANGELOG.mdnvd
- github.com/stalwartlabs/stalwart/commit/15762fba2ba335e560b8d25f71af085a8b6b6cf2nvd
- github.com/stalwartlabs/stalwart/releases/tag/v0.13.3nvd
- github.com/stalwartlabs/stalwart/security/advisories/GHSA-xv4r-q6gr-6pfgnvd
- tools.ietf.org/html/rfc4791nvd
News mentions
0No linked articles in our index yet.