check / check (push) Failing after 1s
Adds the blocked_networks config key: a list of CIDRs, parsed with net/netip, that is added to the built-in list of address ranges the fetcher refuses to contact and can never remove an entry from it. An invalid CIDR aborts startup naming the key and the value. The built-in list gains CGNAT 100.64.0.0/10, IETF protocol assignments 192.0.0.0/24, benchmark 198.18.0.0/15 and NAT64 64:ff9b::/96. Resolved addresses are unmapped before matching, so IPv4-mapped IPv6 forms are caught too. Enforcement stays in the dial-time re-resolution, which is what closes the DNS rebinding window. What a reader would trip over: 192.0.0.0/24 is now blocked but TEST-NET-1 (192.0.2.0/24), which the Fetch tests use as a public upstream, is a different range and stays dialable. The package-level dialer enforces the built-in ranges only; operator entries are applied by the fetcher. Disclosure: one nolint:gochecknoglobals on the immutable built-in prefix list. Model: opus-4-8 (implementation, review); fable-5-1 (landing message)
112 lines
3.2 KiB
Go
112 lines
3.2 KiB
Go
package httpfetcher
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net"
|
|
"net/http"
|
|
"net/netip"
|
|
"testing"
|
|
)
|
|
|
|
// TestIsPrivateIPBlocksSpecialRanges covers the internal and special-use
|
|
// ranges added to the built-in blocklist, in IPv4, IPv6, and IPv4-mapped
|
|
// forms, alongside public controls that must stay reachable.
|
|
func TestIsPrivateIPBlocksSpecialRanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
ip string
|
|
want bool
|
|
}{
|
|
{"cgnat-low", "100.64.0.1", true},
|
|
{"cgnat-high", "100.127.255.254", true},
|
|
{"ietf-protocol", "192.0.0.1", true},
|
|
{"benchmark-low", "198.18.0.1", true},
|
|
{"benchmark-high", "198.19.255.254", true},
|
|
{"nat64", "64:ff9b::1", true},
|
|
{"nat64-embeds-private", "64:ff9b::a00:1", true}, // maps 10.0.0.1
|
|
{"ipv4-mapped-private", "::ffff:10.0.0.1", true},
|
|
{"cloud-metadata", "169.254.169.254", true},
|
|
{"public-v4", "8.8.8.8", false},
|
|
{"test-net-1-public", testPublicHost, false}, // TEST-NET-1, stays public
|
|
{"public-v6", "2001:4860:4860::8888", false},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ip := net.ParseIP(tc.ip)
|
|
if ip == nil {
|
|
t.Fatalf("failed to parse IP %q", tc.ip)
|
|
}
|
|
|
|
got := isPrivateIP(ip)
|
|
if got != tc.want {
|
|
t.Errorf("isPrivateIP(%q) = %v, want %v", tc.ip, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// transportOf returns the *http.Transport backing a fetcher, so a test can
|
|
// exercise the SSRF-safe dialer New installed with the operator blocklist.
|
|
func transportOf(t *testing.T, f *HTTPFetcher) *http.Transport {
|
|
t.Helper()
|
|
|
|
transport, ok := f.client.Transport.(*http.Transport)
|
|
if !ok {
|
|
t.Fatalf("transport is %T, want *http.Transport", f.client.Transport)
|
|
}
|
|
|
|
return transport
|
|
}
|
|
|
|
// TestDialerEnforcesBlockedNetworks proves an operator-supplied
|
|
// blocked_networks entry is enforced by the dialer, in addition to the
|
|
// built-in ranges, while an address outside both stays dialable.
|
|
func TestDialerEnforcesBlockedNetworks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
cfg := DefaultConfig()
|
|
// TEST-NET-2 (198.51.100.0/24) is public to the built-in check, so
|
|
// blocking it can only come from the operator-supplied list.
|
|
cfg.BlockedNetworks = []netip.Prefix{netip.MustParsePrefix("198.51.100.0/24")}
|
|
|
|
transport := transportOf(t, New(cfg))
|
|
|
|
blocked := []string{
|
|
"198.51.100.5:80", // operator-supplied range
|
|
"10.0.0.5:80", // built-in RFC 1918, still enforced
|
|
"100.64.0.1:80", // built-in CGNAT range
|
|
}
|
|
|
|
for _, addr := range blocked {
|
|
t.Run("blocked/"+addr, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := transport.DialContext(context.Background(), "tcp", addr)
|
|
if !errors.Is(err, ErrSSRFBlocked) {
|
|
t.Errorf("DialContext(%q) = %v, want ErrSSRFBlocked", addr, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("public-not-blocked", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A cancelled context makes the dial fail without touching the
|
|
// network; the point is only that a public literal outside every
|
|
// blocked range is not SSRF-blocked.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
|
|
_, err := transport.DialContext(ctx, "tcp", testPublicHost+":80")
|
|
if errors.Is(err, ErrSSRFBlocked) {
|
|
t.Errorf("public target SSRF-blocked with operator list set: %v", err)
|
|
}
|
|
})
|
|
}
|