Browse Source

fix(security): block reserved IP ranges in SSRF filter

Fred 2 days ago
parent
commit
238c5614e5
2 changed files with 42 additions and 11 deletions
  1. 35 11
      internal/urllib/url.go
  2. 7 0
      internal/urllib/url_test.go

+ 35 - 11
internal/urllib/url.go

@@ -12,7 +12,16 @@ import (
 	"strings"
 )
 
-var rfc6598SharedAddressSpacePrefix = netip.MustParsePrefix("100.64.0.0/10")
+var (
+	rfc6598SharedAddressSpacePrefix = netip.MustParsePrefix("100.64.0.0/10")
+	nat64WellKnownPrefix            = netip.MustParsePrefix("64:ff9b::/96")
+	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("fec0::/10"),
+	}
+)
 
 // IsRelativePath reports whether the link is a relative path (no scheme, host, or scheme-relative // form).
 func IsRelativePath(link string) bool {
@@ -192,21 +201,36 @@ func JoinBaseURLAndPath(baseURL, path string) (string, error) {
 	return finalURL, nil
 }
 
-// IsNonPublicIP returns true if the given IP is private, loopback,
-// link-local, multicast, or unspecified.
+// IsNonPublicIP returns true if the given IP is not safe to access from the network.
 func IsNonPublicIP(ip net.IP) bool {
-	if ip == nil {
+	addr, ok := netip.AddrFromSlice(ip)
+	if !ok {
 		return true
 	}
 
-	if addr, ok := netip.AddrFromSlice(ip); ok && rfc6598SharedAddressSpacePrefix.Contains(addr.Unmap()) {
+	return isNonPublicAddr(addr.Unmap())
+}
+
+func isNonPublicAddr(addr netip.Addr) bool {
+	if nat64WellKnownPrefix.Contains(addr) {
+		addrBytes := addr.As16()
+		return isNonPublicAddr(netip.AddrFrom4([4]byte(addrBytes[12:])))
+	}
+
+	if rfc6598SharedAddressSpacePrefix.Contains(addr) {
 		return true
 	}
 
-	return ip.IsPrivate() ||
-		ip.IsLoopback() ||
-		ip.IsLinkLocalUnicast() ||
-		ip.IsLinkLocalMulticast() ||
-		ip.IsMulticast() ||
-		ip.IsUnspecified()
+	for _, prefix := range nonPublicIPPrefixes {
+		if prefix.Contains(addr) {
+			return true
+		}
+	}
+
+	return addr.IsPrivate() ||
+		addr.IsLoopback() ||
+		addr.IsLinkLocalUnicast() ||
+		addr.IsLinkLocalMulticast() ||
+		addr.IsMulticast() ||
+		addr.IsUnspecified()
 }

+ 7 - 0
internal/urllib/url_test.go

@@ -276,12 +276,19 @@ func TestIsNonPublicIP(t *testing.T) {
 		{"shared address space IPv4", "100.64.0.1", true},
 		{"loopback IPv4", "127.0.0.1", true},
 		{"link-local IPv4", "169.254.42.1", true},
+		{"Azure WireServer IPv4", "168.63.129.16", true},
+		{"Oracle Cloud metadata IPv4", "192.0.0.192", true},
+		{"benchmarking IPv4", "198.18.0.1", true},
 		{"multicast IPv4", "224.0.0.1", true},
 		{"unspecified IPv6", "::", true},
 		{"loopback IPv6", "::1", true},
 		{"multicast IPv6", "ff02::1", true},
+		{"site-local IPv6", "fec0::1", true},
+		{"NAT64 loopback IPv4", "64:ff9b::7f00:1", true},
+		{"NAT64 link-local IPv4", "64:ff9b::a9fe:a9fe", true},
 		{"public IPv4", "93.184.216.34", false},
 		{"public IPv6", "2001:4860:4860::8888", false},
+		{"NAT64 public IPv4", "64:ff9b::5db8:d822", false},
 	}
 
 	for _, tc := range testCases {