Repository navigation
v1.37.0 — SSRF hardening (S1 + S2 + S5)
Security hardening for outbound requests, from an independent pre-launch review.
What changed
Every outbound HTTP request now dials a pre-validated, IP-pinned connection instead of re-resolving DNS at connect time. This closes three findings:
- S1 — Matrix redirect SSRF. Matrix requests followed redirects against a user-controlled homeserver without re-checking the redirect target. A hostile homeserver could bounce a request to an internal address. Redirects are now re-validated and re-pinned on every hop.
- S2 — DNS-rebinding TOCTOU. Mastodon, Bluesky, Matrix, Discord, OAuth, and alt-text all validated the URL then dialed with a bare httpx client, re-resolving the hostname in between. All now route through the pinned transport.
- S5 — CGNAT not blocked. The SSRF blocklist missed 100.64.0.0/10 (shared address space used by cloud providers and Tailscale). Added explicitly (kept the explicit checks rather than a naive is_global, which on Python 3.14 would have allowed multicast and NAT64).
The structural fix
- New
pinned_request(): the single validated outbound entry point. Validates + pins the IP, optionally follows redirects with per-hop re-validation, and strips credentials (Authorization/Cookie/Proxy-Authorization) on cross-origin hops. - New
unpinned_client(): the explicit single-mode carve-out (LAN vision servers, local webhook receivers) where there is no privilege boundary. - A regression test now forbids bare
httpx.Clientconstruction outside the SSRF machinery, so this class of drift can't silently return.
Self-hosted single-mode behavior is unchanged: LAN/loopback destinations work exactly as before.
Full: 1313 sqlite + 26 Postgres tests.