fix(ssrf): implement effectiveIP function to normalize IPv4-in-IPv6 addresses

pull/3487/head
Aaron Liu 2 months ago
parent bce08f88e9
commit 1c5cad6dec

@ -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)
}

Loading…
Cancel
Save