From 1c5cad6dec7ec3037c6479e3a26a3909995d16a2 Mon Sep 17 00:00:00 2001 From: Aaron Liu Date: Wed, 15 Jul 2026 15:28:53 +0800 Subject: [PATCH] fix(ssrf): implement effectiveIP function to normalize IPv4-in-IPv6 addresses --- pkg/request/ssrf.go | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/pkg/request/ssrf.go b/pkg/request/ssrf.go index 02c8d6dd..bc919b63 100644 --- a/pkg/request/ssrf.go +++ b/pkg/request/ssrf.go @@ -1,7 +1,9 @@ package request import ( + "bytes" "context" + "encoding/binary" "errors" "fmt" "net" @@ -187,6 +189,9 @@ func parseCIDRs(raw []string) []*net.IPNet { } func checkIPWithAllowlist(ip net.IP, allowed []*net.IPNet) error { + // Normalize IPv4-in-IPv6 transition forms so both the allowlist and the + // class check operate on the address the packet actually reaches. + ip = effectiveIP(ip) for _, n := range allowed { if n.Contains(ip) { return nil @@ -195,7 +200,73 @@ func checkIPWithAllowlist(ip net.IP, allowed []*net.IPNet) error { return checkIP(ip) } +// nat64WellKnownPrefix is 64:ff9b::/96 (RFC 6052 §2.1), the 12-byte prefix +// that wraps an IPv4 address in its last 32 bits for NAT64 gateways. +var nat64WellKnownPrefix = []byte{ + 0x00, 0x64, 0xff, 0x9b, + 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, +} + +// teredoPrefix is 2001:0000::/32 (RFC 4380). In Teredo, the client's IPv4 is +// the last 32 bits of the address XORed with 0xff. +var teredoPrefix = []byte{0x20, 0x01, 0x00, 0x00} + +// effectiveIP unwraps IPv4-in-IPv6 transition forms to the IPv4 address the +// packet ultimately reaches, so checkIP classifies the real target rather +// than the (often globally-routable) IPv6 wrapper. Go's net.IP builtins +// (IsLoopback, IsPrivate, IsLinkLocalUnicast, ...) inspect only the outer +// address and would otherwise accept, for example, 64:ff9b::a9fe:a9fe as a +// safe public IPv6 even though it routes to 169.254.169.254 on a NAT64 host. +// +// Covers: +// - IPv4-mapped ::ffff:a.b.c.d (RFC 4291 §2.5.5.2) +// - NAT64 WKP 64:ff9b::a.b.c.d (RFC 6052 §2.1) +// - 6to4 2002:AABB:CCDD:: (RFC 3056) +// - Teredo 2001:0000:... (RFC 4380) +// - IPv4-compatible ::a.b.c.d (RFC 4291 §2.5.5.1, deprecated) +func effectiveIP(ip net.IP) net.IP { + if ip == nil { + return nil + } + // IPv4 literal or IPv4-mapped ::ffff:a.b.c.d — To4 returns the embedded v4. + if v4 := ip.To4(); v4 != nil { + return v4 + } + v6 := ip.To16() + if v6 == nil { + return ip + } + + // NAT64 well-known prefix 64:ff9b::/96. + if bytes.HasPrefix(v6, nat64WellKnownPrefix) { + return net.IPv4(v6[12], v6[13], v6[14], v6[15]).To4() + } + + // 6to4 2002::/16 — bytes 2-5 encode the embedded IPv4. + if v6[0] == 0x20 && v6[1] == 0x02 { + return net.IPv4(v6[2], v6[3], v6[4], v6[5]).To4() + } + + // Teredo 2001:0000::/32 — client IPv4 is last 4 bytes XOR 0xff. + if bytes.HasPrefix(v6, teredoPrefix) { + return net.IPv4(v6[12]^0xff, v6[13]^0xff, v6[14]^0xff, v6[15]^0xff).To4() + } + + // IPv4-compatible ::a.b.c.d — top 12 bytes are zero. Skip :: and ::1 so + // the outer classifier still catches them as unspecified/loopback IPv6. + var zero [12]byte + if bytes.Equal(v6[:12], zero[:]) { + if last := binary.BigEndian.Uint32(v6[12:16]); last > 1 { + return net.IPv4(v6[12], v6[13], v6[14], v6[15]).To4() + } + } + + return ip +} + func checkIP(ip net.IP) error { + ip = effectiveIP(ip) if ip == nil { return fmt.Errorf("invalid IP: %w", ErrUnsafeURL) }