Browse Source

fix(security): block 6to4 and IPv4-compatible addresses in SSRF filter

IsNonPublicIP already decoded NAT64 (64:ff9b::/96) addresses, but other
IPv6 forms embedding an IPv4 destination were classified as public, so
URLs like http://[2002:7f00:1::]/ or http://[::10.0.0.1]/ bypassed the
private network check.

Decode the IPv4 address embedded in 6to4 (2002::/16) addresses and
check it like NAT64. Block the deprecated IPv4-compatible range (::/96)
entirely instead of decoding it, since decoding would classify ::1 as
the public 0.0.0.1.
Fred 16 hours ago
parent
commit
3945f3daa0
2 changed files with 15 additions and 0 deletions
  1. 7 0
      internal/urllib/url.go
  2. 8 0
      internal/urllib/url_test.go

+ 7 - 0
internal/urllib/url.go

@@ -15,10 +15,12 @@ import (
 var (
 	rfc6598SharedAddressSpacePrefix = netip.MustParsePrefix("100.64.0.0/10")
 	nat64WellKnownPrefix            = netip.MustParsePrefix("64:ff9b::/96")
+	sixToFourPrefix                 = netip.MustParsePrefix("2002::/16")
 	nonPublicIPPrefixes             = []netip.Prefix{
 		netip.MustParsePrefix("168.63.129.16/32"),
 		netip.MustParsePrefix("192.0.0.192/32"),
 		netip.MustParsePrefix("198.18.0.0/15"),
+		netip.MustParsePrefix("::/96"), // Deprecated IPv4-compatible IPv6 addresses (RFC 4291).
 		netip.MustParsePrefix("fec0::/10"),
 	}
 )
@@ -217,6 +219,11 @@ func isNonPublicAddr(addr netip.Addr) bool {
 		return isNonPublicAddr(netip.AddrFrom4([4]byte(addrBytes[12:])))
 	}
 
+	if sixToFourPrefix.Contains(addr) {
+		addrBytes := addr.As16()
+		return isNonPublicAddr(netip.AddrFrom4([4]byte(addrBytes[2:6])))
+	}
+
 	if rfc6598SharedAddressSpacePrefix.Contains(addr) {
 		return true
 	}

+ 8 - 0
internal/urllib/url_test.go

@@ -286,9 +286,17 @@ func TestIsNonPublicIP(t *testing.T) {
 		{"site-local IPv6", "fec0::1", true},
 		{"NAT64 loopback IPv4", "64:ff9b::7f00:1", true},
 		{"NAT64 link-local IPv4", "64:ff9b::a9fe:a9fe", true},
+		{"6to4 loopback IPv4", "2002:7f00:1::", true},
+		{"6to4 private IPv4", "2002:a00:1::", true},
+		{"6to4 link-local IPv4", "2002:a9fe:a9fe::", true},
+		{"IPv4-compatible loopback", "::127.0.0.1", true},
+		{"IPv4-compatible private", "::10.0.0.1", true},
+		{"IPv4-compatible link-local", "::169.254.169.254", true},
+		{"IPv4-compatible public", "::93.184.216.34", true},
 		{"public IPv4", "93.184.216.34", false},
 		{"public IPv6", "2001:4860:4860::8888", false},
 		{"NAT64 public IPv4", "64:ff9b::5db8:d822", false},
+		{"6to4 public IPv4", "2002:5db8:d822::1", false},
 	}
 
 	for _, tc := range testCases {