Guard the remaining IPv6 special ranges; test refusal at the dialer
Codex's review of the guard: RFC 8215's local-use NAT64 prefix (64:ff9b:1::/48) was missing, and a local translator may point it at private IPv4 space; the discard, benchmarking and second documentation prefixes were missing too. All are in the CIDR list now. The transport test only ever dialled localhost by name; the new test calls the transport's own DialContext for private, mapped, NAT64 and reserved literals and requires the guard's refusal — which comes from Dialer.Control, before connect(2), so no packet leaves and no timeout is waited for.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2 files changed,
+35
−6
ssrf.go+10 −6ssrf_test.go+25 −0
diff --git a/ssrf.go b/ssrf.go| index 32db1f9..7fb1620 100644 |
| --- a/ssrf.go |
| +++ b/ssrf.go |
| @@ -37,17 +37,21 @@ func validateEndpoint(raw string) error { |
| return nil |
| } |
| -// reservedNets are the ranges net.IP's own predicates do not cover: |
| -// "this network", IETF protocol assignments, the documentation and |
| -// benchmarking nets, class E, IPv6 documentation, and the NAT64 |
| -// prefix (whose low 32 bits are an IPv4 address the IPv4 rules would |
| -// otherwise never see; no push service lives behind it). |
| +// reservedNets are the ranges net.IP's own predicates do not cover, |
| +// from IANA's special-purpose registries: "this network", IETF |
| +// protocol assignments, the documentation and benchmarking nets, |
| +// class E; and for IPv6 the discard prefix, benchmarking, both |
| +// documentation prefixes, and the NAT64 prefixes — the well-known one |
| +// and RFC 8215's local-use one, whose low 32 bits are an IPv4 address |
| +// the IPv4 rules would otherwise never see, and which a local |
| +// translator may point at private space. |
| var reservedNets = func() []*net.IPNet { |
| var out []*net.IPNet |
| for _, c := range []string{ |
| "0.0.0.0/8", "192.0.0.0/24", "192.0.2.0/24", "198.18.0.0/15", |
| "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4", |
| - "2001:db8::/32", "64:ff9b::/96", |
| + "64:ff9b::/96", "64:ff9b:1::/48", "100::/64", "2001:2::/48", |
| + "2001:db8::/32", "3fff::/20", |
| } { |
| _, n, err := net.ParseCIDR(c) |
| if err != nil { |
diff --git a/ssrf_test.go b/ssrf_test.go| index dff2520..ecebfe7 100644 |
| --- a/ssrf_test.go |
| +++ b/ssrf_test.go |
| @@ -8,6 +8,7 @@ import ( |
| "net/http/httptest" |
| "strings" |
| "testing" |
| + "time" |
| ) |
| func TestValidateEndpoint(t *testing.T) { |
| @@ -44,6 +45,7 @@ func TestGuardedIP(t *testing.T) { |
| // Reserved ranges the net.IP predicates do not cover. |
| "0.1.2.3", "192.0.0.1", "192.0.2.1", "198.18.0.1", "198.19.255.255", "198.51.100.1", |
| "203.0.113.1", "240.0.0.1", "255.255.255.255", "2001:db8::1", "64:ff9b::a00:1", |
| + "64:ff9b:1::a00:1", "100::1", "2001:2::1", "3fff::1", |
| "::", "224.0.0.1", "ff02::1", |
| } |
| for _, ip := range refused { |
| @@ -74,6 +76,29 @@ func TestClientRefusesLoopbackAtDial(t *testing.T) { |
| } |
| } |
| +// The transport's own dialer must refuse before any packet leaves: the |
| +// guard runs in Dialer.Control, which precedes connect(2), so each of |
| +// these fails instantly with the guard's error rather than a timeout. |
| +// Ports 9 (discard) would otherwise sit there. |
| +func TestClientDialerRefusesPrivateAddressesBeforeConnecting(t *testing.T) { |
| + tr := newClient().Transport.(*http.Transport) |
| + for _, addr := range []string{ |
| + "10.0.0.1:443", "172.16.5.5:443", "192.168.1.1:443", "169.254.169.254:80", |
| + "[::ffff:10.0.0.1]:443", "[64:ff9b::a00:1]:443", "[64:ff9b:1::a00:1]:443", |
| + "[fc00::1]:443", "[2001:db8::1]:443", "192.0.2.1:443", "127.0.0.1:9", |
| + } { |
| + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) |
| + conn, err := tr.DialContext(ctx, "tcp", addr) |
| + cancel() |
| + if conn != nil { |
| + conn.Close() |
| + } |
| + if err == nil || !strings.Contains(err.Error(), "aviso: dial refused") { |
| + t.Errorf("%s: got %v, want the guard's refusal", addr, err) |
| + } |
| + } |
| +} |
| + |
| func TestClientRefusesRedirects(t *testing.T) { |
| c := newClient() |
| req, _ := http.NewRequest(http.MethodGet, "https://example.invalid/", nil) |