fix(oauth): close nine address-classification bypasses in the shared SSRF guard master
`isPrivateIP` is the address classifier inside `NewSSRFSafeHTTPClient`, the guard four subsystems already route their attacker-influenced fetches through (oauth, core/blobs, core/blueskypost, atproto/jetstream/authorpost). It enumerated loopback, link-local and the RFC1918 blocks and treated everything else as public and dialable — and the address space holds far more reserved territory than RFC1918. Nine classes were reachable. Each was confirmed by observation, not inference; the acceptance test stands up a real loopback listener, addresses it as `0.0.0.0:<port>`, and asserts the handler was never invoked — it was, before this change, on both Linux and darwin, because `connect(0.0.0.0)` is substituted with the local host by the kernel. The classifier is now strictly stronger with zero caller churn. No signature changed; the four dependent subsystems inherit the fix. Changes: - Block the unspecified address in both families (`0.0.0.0`, `::`, `::ffff:0.0.0.0`) — a wildcard in bind() only, loopback in connect(). - Block CGNAT `100.64.0.0/10`, IETF protocol assignments `192.0.0.0/24`, benchmarking `198.18.0.0/15`, reserved `240.0.0.0/4` (which carries the `255.255.255.255` broadcast), and `0.0.0.0/8`. - Block multicast via the family-agnostic `ip.IsMulticast()` rather than a `224.0.0.0/4` entry, which is IPv4-only and would have left the `ff05::` and `ff0e::` scopes open. - Block deprecated IPv6 site-local `fec0::/10`, which falls outside both predicates that look like they cover it (`IsPrivate` is `fc00::/7`, `IsLinkLocalUnicast` is `fe80::/10`). - Decode and re-check IPv6 forms carrying an IPv4 payload: NAT64 well-known `64:ff9b::/96`, NAT64 local-use `64:ff9b:1::/48` (RFC 8215), SIIT IPv4-translated `::ffff:0:0:0/96`, and IPv4-compatible `::/96`. - Ban 6to4 `2002::/16` outright instead of decoding it. Its embedded IPv4 names a tunnel GATEWAY, not the destination, so a public payload says only who the tunnel belongs to. NAT64 embeds the destination and is therefore decoded, not banned — making the two symmetric is wrong in either direction, and banning NAT64 would break all outbound federation on an IPv6-only host behind DNS64. - Replace the per-call `net.ParseCIDR` loop with stdlib predicates plus a package-level list parsed once, removing five allocations per address per request from the hot path. Tests (T0, in-package, testify, parallel): - Acceptance contract: the client refuses to reach a real listener addressed as `0.0.0.0:<port>`, asserting the handler was never invoked rather than merely that an error was returned. - Classification tables with boundary rows one address outside each new range, so a mask written one bit too wide fails. - Embedded-payload table whose `2002:808:808::`-style public-payload rows are what force a decode rather than a wholesale prefix ban. - A tripwire on the range list itself: no entry may contain a public address. This catches CIDR degeneration generically — `net.ParseCIDR("::ffff:0:0/96")` parses as `0.0.0.0/0` and would refuse every outbound request the AppView makes. - Characterization tests for two properties that already held but were unasserted: per-hop redirect re-vetting, and rejection of a DNS answer mixing public and private addresses in either order. - Coverage for the fail-closed dial path taken when the transport is bypassed. Verified: `make ci` green (5496 tests, 0 failed, 0 skipped), T0 and T1 green, `go vet` clean, `make test-audit` at 0 violations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>