Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
0b1e8f8
fix: don't place time-range under VCALENDAR in comp-type-less search
tobixen Jun 3, 2026
f5c6278
feat: add compatibility_workarounds flag to search()
tobixen Jun 3, 2026
a8b08c2
test: integration test + forward compatibility_workarounds in Calenda…
tobixen Jun 3, 2026
b5ff52d
fix: apply async search driver exception-forwarding and #681 mirror test
tobixen Jun 4, 2026
951a463
docs: CHANGELOG for issue #681 fix and compatibility_workarounds flag
tobixen Jun 4, 2026
0cfd154
chore: calibrate comp-type-optional hints for issue #681 split
tobixen Jun 4, 2026
fb27cdb
fix: correct time-range.comp-type.optional hints after stricter probe
tobixen Jun 4, 2026
1aa6978
fix: split comp-type-less searches that carry a property filter
tobixen Jun 4, 2026
3824521
fix: rename comp-type-optional features to avoid grouping-node pollution
tobixen Jun 5, 2026
5d0f9bb
test: gate the comp-type-less time-range Run 2 on a ReportError rejec…
tobixen Jun 5, 2026
d0a6b54
fix: _derive_from_subfeatures returns None when has_positive but inco…
tobixen Jun 5, 2026
8bca39f
refactor(tests): consolidate derivation tests into TestImplicitDeriva…
tobixen Jun 5, 2026
82fb753
fix: correct comp-type.optional hints after server-tester checker fix
tobixen Jun 6, 2026
83e0dd0
fix: correct create-calendar.set-displayname expectation for zimbra a…
tobixen Jun 6, 2026
3b89603
docs(tests): remove bogus TEST_<SERVER> env vars from test docs
tobixen Jun 7, 2026
3da02af
test: add save-load.stable-url compatibility feature; fix save-load g…
tobixen Jun 7, 2026
41a6582
test: use near-now dates so save-then-search event tests pass on OX
tobixen Jun 7, 2026
0c3a92c
test: mirror near-now date fix to async OX save-then-search tests
tobixen Jun 7, 2026
db062e5
fix: Zimbra create-calendar.set-displayname is fragile, not full
tobixen Jun 7, 2026
fdf1b39
test: add save-load.put-overwrite and save-load.mutable.attendee-part…
tobixen Jun 7, 2026
94faf23
fix: nest if-match-optional under save-load.mutable and unify parent …
tobixen Jun 7, 2026
3da9398
feat: model display-name/URL coupling via create-calendar.set-display…
tobixen Jun 8, 2026
c21d724
test: tag directly-probed compatibility nodes as independent features
tobixen Jun 8, 2026
7848ed0
fix: correct OX and CCS recurrence-search support in compatibility ma…
tobixen Jun 8, 2026
11a2eaf
fix: Stalwart expand/exception is fragile (SEQUENCE-dependent); guard…
tobixen Jun 8, 2026
4e7780c
test: CCS and SOGo features unblocked by near-future test fixtures
tobixen Jun 9, 2026
68c53e3
docs: minor CONTRIBUTION.md tweak
tobixen Jun 9, 2026
2be0cd5
test: make display-name fixtures idempotent under wipe-calendar + uni…
tobixen Jun 9, 2026
0663526
fix: correct OX compatibility metadata for comp-type, is-not-defined …
tobixen Jun 9, 2026
36600cb
test: search near-future anniversary in recurring-date search tests (OX)
tobixen Jun 9, 2026
04605d2
test: correlate inbox item by UID in testAcceptInviteUsernameEmailFal…
tobixen Jun 9, 2026
abc8aec
test: correlate inbox item by UID in (sync+async) invite-and-respond too
tobixen Jun 9, 2026
f5c7f96
test: anchor edit-single-recurrence test near now (CCS old-dates)
tobixen Jun 9, 2026
8f4e8ac
test: migrate calendar_color/calendar_order old_flags to probed features
tobixen Jun 10, 2026
ad279a8
test: migrate propfind_allprop_failure to propfind.allprop.resourcetype
tobixen Jun 10, 2026
5510e33
test: migrate duplicates_not_allowed to save.duplicate-event
tobixen Jun 10, 2026
c9b0309
test: migrate non_existing_raises_other to non-existing-raises-not-found
tobixen Jun 10, 2026
cc08793
test: migrate vtodo_datesearch_nodtstart_task_is_skipped to search.ti…
tobixen Jun 10, 2026
f0d024c
fix: accept config file sections with features but no caldav_url
tobixen Jun 10, 2026
672a8c0
feat: make extract_conn_params_from_section public API
tobixen Jun 11, 2026
b8537ee
docs: add full codebase review, June 2026
tobixen Jun 11, 2026
22b9cc6
fix: COMPLETED date fixup corrupted the following iCal property line
tobixen Jun 11, 2026
185b98b
fix: 302 response to PUT raised IndexError instead of following redirect
tobixen Jun 11, 2026
0b28d77
fix: URL.canonical() leaked credentials and mutated self on __eq__
tobixen Jun 11, 2026
685d837
fix: combined-is-logical-and workaround silently dropped property fil…
tobixen Jun 11, 2026
4aa29f1
fix: stale DataState cache after _set_data, and FREEBUSY component sniff
tobixen Jun 11, 2026
f1bc150
fix: require_tls=True not enforced on well-known URI redirect target
tobixen Jun 11, 2026
767f2cc
fix: XML parser in response.py lacked entity hardening
tobixen Jun 11, 2026
4332bfe
fix: seven crash/wrong-result bugs from code review §1.6–§1.9, §2.13,…
tobixen Jun 11, 2026
7c1dbda
fix: three more crash bugs from code review §1.10, §1.11, §1.12
tobixen Jun 11, 2026
4a8ddef
fix: jmap create_task bare KeyError when server returns empty created…
tobixen Jun 11, 2026
9cd43c9
fix: four silent wrong-result bugs §2.11, §2.12, §2.18, §2.19
tobixen Jun 11, 2026
a3b7258
fix: §1.2 URL+password TypeError, §1.3 rate-limit None+float, §2.7 un…
tobixen Jun 11, 2026
732cb25
fix: async aio.get_calendars(calendar_name=...) never found any calen…
tobixen Jun 11, 2026
a5b2b10
fix: three bugs from June 2026 code review (§1.5, §2.8, §2.15)
tobixen Jun 11, 2026
e30e4f6
fix: four JMAP converter bugs from June 2026 code review (§4.1–§4.4)
tobixen Jun 12, 2026
3d623d5
fix: JMAP update_event never cleared removed properties (§4.5)
tobixen Jun 12, 2026
ea84017
fix: create_ical ical_fragment injected into VALARM instead of VEVENT…
tobixen Jun 13, 2026
643566d
fix: trailing-whitespace fix in vcal.fix() was dead code — add re.MUL…
tobixen Jun 13, 2026
4bea4a2
fix: backslash-unescape regex in vcal.fix() was a no-op for lone quot…
tobixen Jun 13, 2026
328b39c
fix: §3.3 COMMDUMP warning; §4.6 JMAP search UTCDate; §4.7 return new…
tobixen Jun 13, 2026
2bb9e5f
docs: add COMMDUMP security note from v1.4.0 changelog to SECURITY.md
tobixen Jun 13, 2026
d50fdd1
docs: mark §2.2–2.4, §3.3, §4.6–4.7 as FIXED in code review document
tobixen Jun 13, 2026
1633e83
docs: SECURITY.md — fix typos, add reporting process, SSRF and XML-pa…
tobixen Jun 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .lycheeignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
https?://your\.server\.example\.com/.*
https?://.*\.example\.com(:\d+)?(/.*)?$
https?://domain/.*
https?://evil.attacker.com/caldav/

# Localhost URLs for test servers (not accessible in CI)
http://localhost:\d+/.*
Expand Down
65 changes: 65 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The types used should (as for now) be one of:
The `compatibility_hints.py` has been moved from the test directory to the codebase not so very long ago. Some special rules here:

* Adjusting the feature set for some calendar server? Check if there exists some workarounds etc in the code for said feature, if so, then it should be considered a fix or a feature. Perhaps even a breaking change. Otherwise, use `test: ...`. (because it is relevant for the compatibility test, if nothing else).
* Adding a new feature hint? Ensure it's covered by the caldav-server-tester. Since we have a compatibility test, it will be relevant for the test - so use `test: (...)`. It should be covered by the caldav-serveer-tester, so refer to some issue or pull request for the caldav-server-tester in the commit message.
* Adding a new feature hint? Ensure it's covered by the caldav-server-tester. Since we have a compatibility test, it will be relevant for the test - so use `test: (...)`. It should be covered by the caldav-server-tester.
* Changing some descriptions? That goes as `docs: ...` even if it's actually changing a variable in the code.

This is not set in stone. If you feel strongly for using something else, use something else in the commit message and update this file in the same commit.
Expand Down
70 changes: 52 additions & 18 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,48 +1,82 @@
# Security policy

Issues should be fixed ASAP, and information on any security issue should be published as soon as it's fixed. Use the GitHub issue tracker or check up [CONTACT](CONTACT.md) or even the [CODE OF CONDUCT](CODE_OF_CONDUCT) file to get in touch with the maintainer.
Issues should be fixed ASAP, and information on any security issue should be published as soon as it's fixed. Serious issues should be reported privately and kept under wraps until a fix is released — use [GitHub's private vulnerability reporting](https://github.com/python-caldav/caldav/security/advisories/new) (the "Report a vulnerability" button on the Security tab), or get in touch with the maintainer via [CONTACT](CONTACT.md) or the [CODE OF CONDUCT](CODE_OF_CONDUCT) file. Use the public GitHub issue tracker only for non-sensitive issues.

There are no "LTS"-releases of the CalDAV package, but the maintainer will always consider backporting security fixes if it's deemed relevant. The maintainer is doing most of the maintenance on hobby-basis and may have other things in life preventing him from dealing with issues on the go, so no guarantees are given.

All contributions are carefully reviewed by the maintainer, and all releases are carefully tested and tagged with a PGP-signed commit.
All contributions are carefully reviewed by Tobias Brox, and AI-tools are used for code reviews prior to each release. All releases are carefully tested and tagged with a PGP-signed commit.

# Known security issues and risks

## RFC6764

I do see a major security flaw with the RFC6764 discovery. If the DNS is not to be trusted, someone can highjack the connection by spoofing the service records, and also spoofing the TLS setting, encouraging the client to connect over plain-text HTTP without certificate validation. Utilizing this it may be possible to steal the credentials. This flaw can be mitigated by using DNSSEC, but DNSSEC is not widely used, and fixing support for DNSSEC validation in the CalDAV library was found to be non-trivial (perhaps I'll look into it again some time after 3.0 has been released). This has been mitigated by adding a require_tls` connection parameter that is True by default, plus by ensuring one isn't routed to a different domain.
**Summary**: If you're using auto-discovery of the CalDAV-URL, anyone controlling your local resolver may try to fish out username and password.

## DDoS/OOM risk
**Mitigation**: Use a URL rather than a domain when configuring the library. Leave `require_tls`, `ssl_verify_cert` to the default.

I do see a major security flaw with the RFC6764 discovery. If the DNS is not to be trusted, someone can hijack the connection by spoofing the service records, and also spoofing the TLS setting, encouraging the client to connect over plain-text HTTP without certificate validation. Utilizing this it may be possible to steal the credentials. This flaw can be mitigated by using DNSSEC, but DNSSEC is not widely used, and fixing support for DNSSEC validation in the CalDAV library was found to be non-trivial - the work done is now on a stalled pull request. This has been partly mitigated by adding a `require_tls` connection parameter that is `True` by default, plus by ensuring one isn't routed to a different domain.

Auto-discovery and HTTP redirects also mean the host you end up talking to may not be the one you configured. If you run the library in a context where it can reach internal/private network resources (server-side request forgery, SSRF), be aware that a malicious DNS resolver or a malicious/compromised server can steer requests towards other hosts. Configuring an explicit URL rather than relying on domain-based discovery limits this.

## DDoS/OOM risk - recurring events/tasks search

**Summary:** If you allow untrusted parties to specify search-terms towards a calendar containing recurring events/tasks, bad things may happen.

The package offers both client-side and server-side expansion of recurring events and tasks. It currently does not offer expansion for open-ended date searches - but with a large enough timespan and a frequent enough RRULE, there may be millions of recurrences returned. Those recurrences are returned as a generator, so things will not break down immediately. However, there is no guaranteed sort order of the recurrences ... and once you add sorting parameters to the search, bad things may happen.

## XML parsing

**Summary:** XML responses are parsed defensively by default; only relax this against servers you trust.

The library parses XML responses from the server using lxml. By default the parser is configured to resist common XML attacks: external entity resolution is disabled (`resolve_entities=False`) and network access during parsing is blocked (`no_network=True`), guarding against XXE (XML External Entity) attacks, and lxml's built-in limits protect against oversized "billion laughs"-style entity-expansion payloads.

The `huge_tree` connection option (default off) disables lxml's built-in parser limits so that very large calendar objects can be handled. With `huge_tree` enabled, a malicious or compromised server can exhaust available memory with a crafted XML payload — only enable it against servers you trust. See the [lxml XMLParser documentation](https://lxml.de/api/lxml.etree.XMLParser-class.html).

## Bugs causing weird things happening

Weird things may happen due to bugs both on in the CalDAV package, on your side and on the server side. Here are some weird experiences with Zimbra:
**Summary:** Always expect the unexpected

Weird things may happen due to bugs both on in the CalDAV package, on your side and on the server side. Some anecdotes from using Zimbra:

* I have experiences that cancelling participation in an event caused the event to be cancelled for all participants (even if the person deciding to not go to the event was not an organizer and should have no permissions to edit the event). Clearly a server-side issue.
* I once tried to restore from backup and push ten years of ical code to the calendar server. The calendar server responded by re-inviting people to the meetings we had ten years ago. I'm inclined to call that also a server side bug.
* Many other things may happen.
* I once tried to restore from backup and push ten years of ical code to the calendar server. The calendar server responded by re-inviting people to the meetings we had ten years ago. I'm inclined to call that a server side bug - but it also highlights the risk of using the CalDAV library for doing operations that ordinary calendaring clients aren't doing.
* It's been observed that cancelling participation in an event caused the event to be cancelled for all participants (even if the person deciding to not go to the event was not an organizer and should have no permissions to edit the event). Clearly a server-side issue.

## Malicious usage
## Other things to consider

Beware of risks and exposure when creating applications:
**Summary:** Beware of risks and exposure when creating applications:

* Your code may handle username and password, be careful not to expose such credentials. Even the URL to the calendar server and/or calendar may be something people want to keep private.
* Your code may handle username and password, be careful not to expose such credentials. Even the URL to the calendar server and/or calendar may be something people want to keep private. The library includes code for reading this data from a standard config file - please use it rather than reinventing the wheel or hard-coding credentials directly into your code.
* Consider that calendar events and such is personal data, which deserves protection. In the EU with the GDPR, such protection is even mandated by law.
* If you allow arbitrary people to create calendar content to be saved to a server, there may be some risks involved:
* Depending on the server implementation, it may be possible to use the caldav library for sending spam emails.
* Be aware of DoS-attacks: By storing too much / too big / specially crafted icalendar data, the server and/or client may crash or consume all available resources.
* If allowing anonymous parties to save and retrieve data from your server, you may end up with responsibility for spreading illicit information. This may include things like child porn. Political or religious propaganda may be legitimate and legal in some countries, but may involve death penalty in other countries. Your calendar server may also be used for coordinating criminal activity.
* If you allow arbitrary people to fetch calendar content from the server, there may also be some risks involved - in particular, a DoS-attack by requesting a large time span of expanded events.
* If allowing anonymous parties to save and retrieve data from your server, you may end up with responsibility for spreading illicit information. This may include things like child sexual abuse material. Political or religious propaganda may be legitimate and legal in some countries, but may involve death penalty in other countries. Your calendar server may also be used for coordinating criminal activity.
* If you allow arbitrary people to fetch calendar content from the server, there may also be some risks involved - see the separate section on DoS-attack by requesting a large time span of expanded events.

## Malicious code
## Supply attack risk

All code contributions are carefully reviewed by Tobias Brox. Version tags are signed with PGP. Of course there is always a risk that someone takes over my PGP key and github access (It's hard to be immune against a [5$ wrench attack](https://xkcd.com/538/)). The original owner of the repository is still alive and may take over the project again should something happen to me. I would anyway encourage using AI to do risk assessments.
**Summary:** Stick to released versions and check the PGP signature in the release-tag

All code contributions are carefully reviewed by Tobias Brox. Version tags are signed with PGP. Of course there is always a risk that someone takes over my PGP key and GitHub access (It's hard to be immune against a [$5 wrench attack](https://xkcd.com/538/)). The original owner of the repository is still alive and may take over the project again should something happen to me. I would encourage using AI to do risk assessments.

The library comes with a number of dependencies, one may need to evaluate the security of those too. The pyproject contains the current list. Some notes:

* niquests is an optional dependency - you may replace it with requests if you don't trust niquests
* recurring-ical-events and icalendar both has the same maintainer (Nicco Kunzmann). He is considered trustworthy.
* Tobias now has a policy of moving code not related to CalDAV into separate packages. Packages under the `python-caldav` ownership on GitHub should be considered to be of the same quality and security level as the CalDAV library.
* No security review have been done of the other dependencies.
* recurring-ical-events and icalendar both have the same maintainer (Nicco Kunzmann). He is considered trustworthy.
* Tobias now has a policy of moving code not related to CalDAV into separate packages. Those packages are most of the time either published under the `python-caldav` or `pycalendar` ownership on GitHub, and should be considered to be of the same quality and security level as the CalDAV library.
* No independent security review has been done of the other dependencies - those are all considered to be mature and robust projects.

## Communication dumper debug hook

**Summary:** If someone has the ability to both alter the environment and full read access to /tmp (basically, someone has root access to the computer where the code is run), it will be possible to get access to all communication. Also, anyone using this debug hook must take responsibility of deleting the dumped files.

**Mitigation:** If this worries you, set `caldav.lib.error.debug_dump_communication=False` after importing caldav.

The following was written when `PYTHON_CALDAV_COMMDUMP` was introduced in v1.4.0:

* An attacker that has access to alter the environment the application is running under may cause a DoS-attack, filling up available disk space with debug logging.
* An attacker that has access to alter the environment the application is running under, and access to read files under /tmp (files being 0600 and owned by the uid the application is running under), will be able to read the communication between the server and the client, communication that may be private and confidential.

Thinking it through three times, I'm not too concerned — if someone has access to alter the environment the process is running under and access to read files run by the uid of the application, then this someone should already be trusted and will probably have the possibility to DoS the system or gather this communication through other means.

As of v3.3 (to be released towards the end of 2026-06), a warning is logged at import time when this variable is set, reminding the operator that request/response bodies and headers (including credentials and calendar PII) are written to uniquely-named files under `/tmp` that accumulate indefinitely.
39 changes: 29 additions & 10 deletions caldav/async_davclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,13 +372,13 @@ async def request(
self.rate_limit_default_sleep,
self.rate_limit_max_sleep,
)
if rate_limit_time_slept:
sleep_seconds += rate_limit_time_slept / 2
if sleep_seconds is None or (
self.rate_limit_max_sleep is not None
and rate_limit_time_slept > self.rate_limit_max_sleep
):
raise
if rate_limit_time_slept:
sleep_seconds += rate_limit_time_slept / 2
await asyncio.sleep(sleep_seconds)
return await self.request(
url, method, body, headers, rate_limit_time_slept + sleep_seconds
Expand Down Expand Up @@ -432,7 +432,7 @@ async def _async_request(
log.debug(f"server responded with {r.status_code} {reason}")
if (
r.status_code == 401
and "text/html" in self.headers.get("Content-Type", "")
and "text/html" in r.headers.get("Content-Type", "")
and not self.auth
):
msg = (
Expand Down Expand Up @@ -484,7 +484,11 @@ async def _async_request(
# Retry original request with auth
request_kwargs["auth"] = self.auth
r = await self.session.request(**request_kwargs)
response = DAVResponse(r, self)
response = DAVResponse(r, self)
else:
# Probe GET did not give us a 401+WWW-Authenticate challenge —
# auth negotiation failed; re-raise the original connection error
raise

# Handle 429/503 rate-limit responses
error.raise_if_rate_limited(r.status_code, str(url_obj), r.headers.get("Retry-After"))
Expand Down Expand Up @@ -955,7 +959,9 @@ async def get_calendars(self, principal: Optional["Principal"] = None) -> list["
)
calendar_home_url = extract_home_set(response.results)
if not calendar_home_url:
return []
# Fall back to the principal URL as calendar home
# (some servers like GMX don't support calendar-home-set)
calendar_home_url = str(principal.url)

# Make URL absolute if relative
calendar_home_url = self._make_absolute_url(calendar_home_url)
Expand Down Expand Up @@ -1267,13 +1273,26 @@ def _try(coro_result, errmsg):
raise

# Fetch specific calendars by name
for cal_name in calendar_names:
if calendar_names:
try:
calendar = await principal.calendar(name=cal_name)
if calendar:
calendars.append(calendar)
all_cals_for_name = await principal.get_calendars()
for cal_name in calendar_names:
for cal in all_cals_for_name:
try:
display_name = await cal.get_display_name()
if display_name == cal_name:
calendars.append(cal)
break
except Exception:
pass
else:
log.error(f"No calendar with name '{cal_name}' found")
if raise_errors:
raise error.NotFoundError(f"No calendar with name '{cal_name}' found")
except error.NotFoundError:
raise
except Exception as e:
log.error(f"Problems fetching calendar by name '{cal_name}': {e}")
log.error(f"Problems fetching calendars by name: {e}")
if raise_errors:
raise

Expand Down
2 changes: 1 addition & 1 deletion caldav/base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -686,7 +686,7 @@ def _try(meth, kwargs, errmsg):
calendar = principal.calendar(cal_url=cal_url)
else:
calendar = principal.calendar(cal_id=cal_url)
if _try(calendar.get_display_name, {}, f"calendar {cal_url}"):
if _try(calendar.get_display_name, {}, f"calendar {cal_url}") is not None:
calendars.append(calendar)

for cal_name in calendar_names:
Expand Down
Loading
Loading