mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-16 09:45:24 -04:00
fix(caldav): disable redirects on the sync/write-back DAVClient (SSRF) (#2663)
validate_caldav_url resolves and vets the initial host, but caldav's niquests session follows 3xx redirects by default, so a validated public URL can be redirected at request time to loopback/link-local/private space, re-opening the SSRF the host check closes. The existing redirect guard only covered the settings test-connection path. Add a shared _build_dav_client helper that pins the session to zero redirects (any 3xx then raises instead of silently following an attacker-chosen Location), and route both the pull (_sync_blocking) and write-back (_writeback_blocking) paths through it. Mirrors the follow_redirects=False already used on the test-connection path. Tests exercise the real DAVClient request path (a 302 toward an internal host is refused, the sink is never contacted; the PROPFIND is asserted to reach the public server first so the check can't pass vacuously), confirm the helper disables redirects on the installed client, guard against a raw DAVClient creeping back in, cover mixed public/internal DNS results in both orderings, and add the resolves-to-no-usable-records fail-closed branch.
This commit is contained in:
+27
-2
@@ -216,18 +216,43 @@ def _open_url_as_calendar(client, url: str):
|
||||
return client.calendar(url=target)
|
||||
|
||||
|
||||
def _build_dav_client(url: str, username: str, password: str):
|
||||
"""Construct a CalDAV client with automatic redirects disabled.
|
||||
|
||||
``validate_caldav_url`` resolves and vets the *initial* host, but caldav's
|
||||
underlying HTTP session follows 3xx redirects by default. So a URL that
|
||||
passes validation can still be redirected — at request time — to
|
||||
loopback / link-local / private space, re-opening the SSRF the host check
|
||||
closes. Pin the session to zero redirects: any 3xx then raises instead of
|
||||
silently following an attacker-chosen ``Location``. This mirrors the
|
||||
test-connection path in ``routes/calendar_routes.py``, which already sets
|
||||
``follow_redirects=False``.
|
||||
|
||||
DAVClient exposes no per-request redirect flag, so we set it on the session
|
||||
after construction (the session is created in ``__init__``).
|
||||
"""
|
||||
import caldav
|
||||
|
||||
client = caldav.DAVClient(url=url, username=username, password=password)
|
||||
# Unconditional: a redirect-disable that only sometimes applies is not a
|
||||
# control. The session exists right after __init__ on every real client;
|
||||
# test_build_dav_client_disables_redirects asserts it against installed
|
||||
# caldav in CI.
|
||||
client.session.max_redirects = 0
|
||||
return client
|
||||
|
||||
|
||||
def _sync_blocking(owner: str, url: str, username: str, password: str, account_id: str = "") -> dict:
|
||||
"""The actual sync — synchronous, intended to run in a threadpool.
|
||||
Returns counts: {calendars, events, deleted, errors}."""
|
||||
# Lazy imports so a missing `caldav` dep doesn't break app startup —
|
||||
# the integrations form still works, sync just no-ops with an error.
|
||||
import caldav
|
||||
from caldav.lib.error import AuthorizationError, NotFoundError
|
||||
from core.database import CalendarCal, CalendarEvent, SessionLocal
|
||||
|
||||
result = {"calendars": 0, "events": 0, "deleted": 0, "errors": []}
|
||||
|
||||
client = caldav.DAVClient(url=url, username=username, password=password)
|
||||
client = _build_dav_client(url, username, password)
|
||||
|
||||
# Discovery: try principal → calendars first; if the server doesn't
|
||||
# support discovery (or the URL points directly at a calendar), fall
|
||||
|
||||
@@ -143,8 +143,10 @@ def _discover_calendars(client):
|
||||
|
||||
def _writeback_blocking(local_cal_id, ev, delete, url, username, password,
|
||||
owner="", account_id="") -> dict:
|
||||
import caldav
|
||||
client = caldav.DAVClient(url=url, username=username, password=password)
|
||||
from src.caldav_sync import _build_dav_client
|
||||
# Redirects disabled here too: the write-back path opens its own DAVClient,
|
||||
# so it needs the same SSRF-via-redirect protection as the pull path.
|
||||
client = _build_dav_client(url, username, password)
|
||||
calendars = _discover_calendars(client)
|
||||
if not calendars:
|
||||
return {"ok": False, "error": "no remote calendars discovered"}
|
||||
|
||||
Reference in New Issue
Block a user