fix(06-13): full IANA special-use SSRF tables and reject zoned dial targets
This commit is contained in:
@@ -155,6 +155,9 @@ func dialControl(policy Policy) func(network, address string, c syscall.RawConn)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("fetchguard: unparseable dial address %q: %w", host, err)
|
return fmt.Errorf("fetchguard: unparseable dial address %q: %w", host, err)
|
||||||
}
|
}
|
||||||
|
if addr.Zone() != "" {
|
||||||
|
return errPrivateIP
|
||||||
|
}
|
||||||
addr = addr.Unmap()
|
addr = addr.Unmap()
|
||||||
if isReservedOrPrivate(addr) {
|
if isReservedOrPrivate(addr) {
|
||||||
return errPrivateIP
|
return errPrivateIP
|
||||||
|
|||||||
@@ -304,3 +304,12 @@ func withTestLoopback(srv *httptest.Server, p Policy) Policy {
|
|||||||
p.skipReservedCheck = true
|
p.skipReservedCheck = true
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDialControlRejectsZonedAndSpecialUse(t *testing.T) {
|
||||||
|
ctl := dialControl(Policy{})
|
||||||
|
for _, a := range []string{"[fe80::1%eth0]:443", "198.18.0.1:443"} {
|
||||||
|
if err := ctl("tcp", a, nil); !errors.Is(err, errPrivateIP) {
|
||||||
|
t.Errorf("%s: got %v", a, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,24 +2,28 @@ package fetchguard
|
|||||||
|
|
||||||
import "net/netip"
|
import "net/netip"
|
||||||
|
|
||||||
// privateV4 is a literal port of ManualCoverUrlFetcher.php PRIVATE_V4_CIDRS.
|
// privateV4 is the IANA IPv4 special-purpose non-public set. It is a strict
|
||||||
var privateV4 = []netip.Prefix{
|
// superset of ManualCoverUrlFetcher.php's lists (06-VERIFICATION gap 3).
|
||||||
netip.MustParsePrefix("127.0.0.0/8"),
|
var privateV4 = mustPrefixes(
|
||||||
netip.MustParsePrefix("10.0.0.0/8"),
|
"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/10", "127.0.0.0/8",
|
||||||
netip.MustParsePrefix("172.16.0.0/12"),
|
"169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24",
|
||||||
netip.MustParsePrefix("192.168.0.0/16"),
|
"192.88.99.0/24", "192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24",
|
||||||
netip.MustParsePrefix("169.254.0.0/16"),
|
"203.0.113.0/24", "224.0.0.0/4", "240.0.0.0/4",
|
||||||
netip.MustParsePrefix("100.64.0.0/10"),
|
)
|
||||||
netip.MustParsePrefix("0.0.0.0/8"),
|
|
||||||
}
|
|
||||||
|
|
||||||
// privateV6 is a literal port of PRIVATE_V6_PREFIXES. PHP lists bare "::1"
|
// privateV6 is the IANA IPv6 special-purpose non-public set. 2002::/16 and
|
||||||
// as a prefix-less loopback literal; it is expressed here as ::1/128 so
|
// 64:ff9b::/96 are handled by embeddedTransitionIPv4 instead.
|
||||||
// Prefix.Contains works uniformly with the CIDR entries.
|
var privateV6 = mustPrefixes(
|
||||||
var privateV6 = []netip.Prefix{
|
"::/96", "100::/64", "2001::/23", "2001:db8::/32", "3fff::/20",
|
||||||
netip.MustParsePrefix("::1/128"),
|
"5f00::/16", "fc00::/7", "fe80::/10", "fec0::/10", "ff00::/8",
|
||||||
netip.MustParsePrefix("fe80::/10"),
|
)
|
||||||
netip.MustParsePrefix("fc00::/7"),
|
|
||||||
|
func mustPrefixes(cidrs ...string) []netip.Prefix {
|
||||||
|
out := make([]netip.Prefix, len(cidrs))
|
||||||
|
for i, c := range cidrs {
|
||||||
|
out[i] = netip.MustParsePrefix(c)
|
||||||
|
}
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -35,7 +39,7 @@ func isReservedOrPrivate(addr netip.Addr) bool {
|
|||||||
if !addr.IsValid() {
|
if !addr.IsValid() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
addr = addr.Unmap()
|
addr = addr.WithZone("").Unmap()
|
||||||
if addr.IsMulticast() || addr.IsUnspecified() {
|
if addr.IsMulticast() || addr.IsUnspecified() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,3 +77,16 @@ func TestIsReservedOrPrivateIPv6Transitions(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsReservedOrPrivateSpecialUseSmoke(t *testing.T) {
|
||||||
|
for _, s := range []string{"198.18.0.1", "192.0.0.1", "240.0.0.1", "255.255.255.255", "2001:db8::1", "fec0::1", "fe80::1%eth0"} {
|
||||||
|
if !isReservedOrPrivate(netip.MustParseAddr(s)) {
|
||||||
|
t.Errorf("%s should be non-public", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range []string{"8.8.8.8", "1.1.1.1", "2606:4700:4700::1111"} {
|
||||||
|
if isReservedOrPrivate(netip.MustParseAddr(s)) {
|
||||||
|
t.Errorf("%s should be public", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user