Skip to content

Commit 84cc15a

Browse files
committed
fix(proxy): unwrap SIIT IPv4-translated IPv6 addresses in SSRF guard
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
1 parent 0435012 commit 84cc15a

2 files changed

Lines changed: 17 additions & 2 deletions

File tree

middleware/proxy/security.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -682,13 +682,15 @@ func isBlockedIPv6TransitionRange(ip net.IP) bool {
682682
// - ISATAP (RFC 5214) — interface identifier 00-00-5E-FE or 02-00-5E-FE
683683
// followed by the embedded IPv4 in the trailing 32 bits. The prefix is an
684684
// ordinary global one, so there is no dedicated range to block wholesale.
685+
// - SIIT IPv4-translated ::ffff:0:0/96 (RFC 2765) — 0xffff marker in bytes
686+
// [8:10], zero in [10:12], embedded IPv4 in the trailing 32 bits.
685687
// - IPv4-compatible ::a.b.c.d (RFC 4291) — trailing 32 bits.
686688
//
687689
// Deprecated tunneling / local-use ranges with operator-dependent embedding
688690
// offsets (6to4, Teredo, NAT64 local-use) are intentionally NOT unwrapped here;
689691
// isBlockedIPv6TransitionRange blocks them wholesale instead. The IPv4-mapped
690-
// form (::ffff:a.b.c.d) is also excluded: net.IP.To4 already surfaces its
691-
// IPv4, so isBlockedIP checks it directly.
692+
// form (::ffff:a.b.c.d, 0xffff marker in bytes [10:12]) is also excluded:
693+
// net.IP.To4 already surfaces its IPv4, so isBlockedIP checks it directly.
692694
func embeddedIPv4(ip net.IP) net.IP {
693695
ip16 := ip.To16()
694696
if ip16 == nil || ip.To4() != nil {
@@ -706,6 +708,15 @@ func embeddedIPv4(ip net.IP) net.IP {
706708
(ip16[8] == 0x00 || ip16[8] == 0x02) {
707709
return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])
708710
}
711+
// SIIT IPv4-translated ::ffff:0:a.b.c.d (RFC 2765, prefix ::ffff:0:0/96):
712+
// the 0xffff marker sits in bytes [8:10] with [10:12] zero, and the
713+
// embedded IPv4 is the trailing 32 bits. This is distinct from the
714+
// IPv4-mapped form (::ffff:a.b.c.d, marker in bytes [10:12]) that
715+
// net.IP.To4 already exposes, so it is unwrapped here.
716+
if allZero(ip16[:8]) && ip16[8] == 0xff && ip16[9] == 0xff &&
717+
ip16[10] == 0x00 && ip16[11] == 0x00 {
718+
return net.IPv4(ip16[12], ip16[13], ip16[14], ip16[15])
719+
}
709720
// IPv4-compatible ::a.b.c.d (first 12 bytes zero). :: and ::1 have already
710721
// been classified above, so treat any remaining all-zero-prefix address as
711722
// a wrapper around its trailing IPv4.

middleware/proxy/security_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ func Test_Security_IsBlockedIP(t *testing.T) {
6161
"2001:db8::5efe:a9fe:a9fe": true, // ISATAP 169.254.169.254 (metadata)
6262
"2001:db8::200:5efe:a00:1": true, // ISATAP 10.0.0.1 (global-scope u/g bit)
6363
"2001:db8::5efe:808:808": false, // ISATAP 8.8.8.8 (public → allowed)
64+
"::ffff:0:7f00:1": true, // SIIT IPv4-translated 127.0.0.1
65+
"::ffff:0:a00:1": true, // SIIT IPv4-translated 10.0.0.1
66+
"::ffff:0:a9fe:a9fe": true, // SIIT IPv4-translated 169.254.169.254 (metadata)
67+
"::ffff:0:808:808": false, // SIIT IPv4-translated 8.8.8.8 (public → allowed)
6468
"8.8.8.8": false,
6569
"1.1.1.1": false,
6670
"93.184.216.34": false, // example.com

0 commit comments

Comments
 (0)