mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-30 00:22:10 -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:
@@ -82,6 +82,39 @@ def test_validate_caldav_url_fails_closed_when_hostname_does_not_resolve(monkeyp
|
||||
caldav_sync.validate_caldav_url("https://calendar.example.com/dav")
|
||||
|
||||
|
||||
def test_validate_caldav_url_fails_closed_when_host_resolves_to_no_usable_records(monkeypatch):
|
||||
# Distinct from the OSError path above: here resolution *succeeds* but yields
|
||||
# no usable A/AAAA records (the `if not addrs` branch). Fail closed there too
|
||||
# rather than letting an un-vetted host through.
|
||||
monkeypatch.setattr(caldav_sync, "_resolve_caldav_host_ips", lambda host: [])
|
||||
|
||||
with pytest.raises(ValueError, match="host does not resolve"):
|
||||
caldav_sync.validate_caldav_url("https://calendar.example.com/dav")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"addrs",
|
||||
[
|
||||
["93.184.216.34", "127.0.0.1"], # public first, internal second
|
||||
["127.0.0.1", "93.184.216.34"], # internal first, public second
|
||||
],
|
||||
)
|
||||
def test_validate_caldav_url_blocks_mixed_dns_in_any_order(monkeypatch, addrs):
|
||||
# A host that resolves to BOTH a public and an internal address must be
|
||||
# rejected regardless of record order — every resolved address is checked,
|
||||
# so one internal answer is enough to block. Defends DNS round-robin and a
|
||||
# rebind that slips an internal A-record alongside a public one.
|
||||
monkeypatch.delenv("ODYSSEUS_ALLOW_PRIVATE_CALDAV", raising=False)
|
||||
monkeypatch.setattr(
|
||||
caldav_sync,
|
||||
"_resolve_caldav_host_ips",
|
||||
lambda host: [ipaddress.ip_address(a) for a in addrs],
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="host is not allowed"):
|
||||
caldav_sync.validate_caldav_url("https://calendar.example.com/dav")
|
||||
|
||||
|
||||
def test_sync_caldav_decrypts_stored_password_and_validates_url(monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
caldav_sync,
|
||||
|
||||
Reference in New Issue
Block a user