🐛 fix(proxy): correctly classify IPv6 transition addresses in upstream validation - #4553
Conversation
The proxy SSRF guard unwrapped IPv4-compatible and NAT64 well-known prefix IPv6 addresses to their embedded IPv4 before applying the blocklist, but missed three other IPv6 transition families that also embed an IPv4 target inside a globally-routable IPv6 literal: - 6to4 (2002::/16, RFC 3056): IPv4 gateway in bytes [2:6] - Teredo (2001:0000::/32, RFC 4380): client IPv4 in bytes [12:16], inverted - NAT64 local-use prefix (64:ff9b:1::/48, RFC 8215): IPv4 in bytes [12:16] An attacker able to influence the proxy upstream URL could encode a private IPv4 (e.g. 169.254.169.254 or 10.0.0.1) in one of these forms to bypass isBlockedIP and reach internal infrastructure (CWE-918). Extend embeddedIPv4 to unwrap all three so the same IPv4 blocklist applies, and add coverage for each family in Test_Security_IsBlockedIP. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBSS2Bm2KRAZmL2A5XfDhu
Address code-review gaps in the previous transition-address fix. The per-form single-offset unwrapping left residual bypasses because several transition families embed their IPv4 at an operator-dependent offset: - NAT64 local-use 64:ff9b:1::/48 (RFC 8215) embeds IPv4 anywhere from a /32 to /96 boundary per RFC 6052; unwrapping only the trailing 32 bits missed the standard /48 embedding. - Teredo 2001:0000::/32 (RFC 4380) can hide a private IPv4 in the un-obfuscated server field (bytes 4:8), not just the client field. Block these deprecated tunneling / local-use ranges wholesale at the CIDR level (6to4 2002::/16, Teredo 2001:0000::/32, NAT64 local-use 64:ff9b:1::/48) via isBlockedIPv6TransitionRange, matching Gitea's and Coder's SSRF guards. None has a legitimate use as a proxy upstream, and RFC 8215 §3 forbids the NAT64 local-use prefix on the public Internet. Keep per-embedded-IP unwrapping only for wrappers whose embedded IPv4 is unambiguous and may legitimately be public: NAT64 well-known prefix (64:ff9b::/96, fixed /96 embedding) and IPv4-compatible ::a.b.c.d. Add ISATAP (RFC 5214) unwrapping, which has no dedicated range to block. Update Test_Security_IsBlockedIP: public-embedded 6to4/Teredo/NAT64-local literals are now blocked wholesale, and add Teredo server-field and ISATAP (private, public, global-scope u/g bit) coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBSS2Bm2KRAZmL2A5XfDhu
A code-review pass surfaced a residual smuggling vector in the same fixed-offset embedded-IPv4 class the guard already covers: the deprecated SIIT IPv4-translated form ::ffff:0:0/96 (RFC 2765). Its 0xffff marker sits in bytes [8:10] (not [10:12] like the IPv4-mapped form), so net.IP.To4 does not surface it and embeddedIPv4 did not unwrap it — leaving e.g. ::ffff:0:7f00:1 (127.0.0.1) and ::ffff:0:a9fe:a9fe (169.254.169.254) reachable past isBlockedIP. Extend embeddedIPv4 to unwrap the trailing 32 bits of ::ffff:0:0/96 and apply the IPv4 blocklist, consistent with the existing NAT64 well-known prefix, ISATAP, and IPv4-compatible handling. Add coverage to Test_Security_IsBlockedIP for private/metadata (blocked) and public (allowed) translated literals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KBSS2Bm2KRAZmL2A5XfDhu
WalkthroughThe proxy SSRF blocklist now blocks additional IPv6 transition ranges, unwraps more fixed-offset embedded IPv4 forms, and recursively validates extracted addresses. Tests expand coverage for NAT64, 6to4, Teredo, ISATAP, and SIIT variants. ChangesSSRF IP blocklist
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Description
By default the proxy middleware only connects upstreams to public addresses and refuses non-public ones (loopback, unspecified, RFC 1918 private, link-local, multicast, and RFC 6598 CGNAT). That decision is made in
isBlockedIP.Several IPv6 "transition" formats embed an IPv4 address inside an IPv6 literal (6to4, Teredo, the NAT64 prefixes, ISATAP, SIIT, and the IPv4-compatible/mapped forms).
isBlockedIPonly recognized two of them — IPv4-compatible::a.b.c.dand the NAT64 well-known prefix64:ff9b::/96— so the remaining forms were classified inconsistently with the IPv4 they wrap. For example::ffff:0:7f00:1and2002:7f00:1::both encode127.0.0.1, yet were treated as ordinary public IPv6 while the bare127.0.0.1was correctly refused.This PR makes the classifier handle the full set of well-known, fixed-prefix embeddings so the same result is produced regardless of which IPv6 form is used to write the address.
Changes introduced
isBlockedIPv6TransitionRangeto reject the deprecated / local-use ranges in their entirety, where the embedded IPv4 offset is operator-dependent and there is no legitimate use as an upstream: 6to42002::/16(RFC 3056, deprecated by RFC 7526), Teredo2001:0000::/32(RFC 4380), and the NAT64 local-use prefix64:ff9b:1::/48(RFC 8215, local-use only).embeddedIPv4to unwrap the embedded IPv4 (and re-apply the same classification) for the forms whose offset is fixed and unambiguous and may legitimately be public: NAT64 well-known prefix64:ff9b::/96(RFC 6052), ISATAP (RFC 5214), SIIT IPv4-translated::ffff:0:0/96(RFC 2765), and IPv4-compatible::a.b.c.d(RFC 4291). The IPv4-mapped form::ffff:a.b.c.dcontinues to be handled directly sincenet.IP.To4surfaces it.Test_Security_IsBlockedIPwith cases for every form above — non-public embeddings are refused, public embeddings (e.g.8.8.8.8wrapped as 6to4/NAT64/ISATAP/SIIT) remain allowed.Behavior note: an upstream literal in the wholesale-blocked ranges (6to4, Teredo, NAT64 local-use) that wraps a public IPv4 is now refused. These are deprecated tunneling / local-use ranges that do not appear as real upstream targets, so this should not affect legitimate usage.
Type of change
Checklist
go test ./middleware/proxy/, including-race).🤖 Generated with Claude Code