Skip to content

v1.37.0 — SSRF hardening (S1 + S2 + S5)

Choose a tag to compare

@jcrabapple jcrabapple released this 02 Sep 22:55
· 373 commits to master since this release

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.Client construction 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.