From 746f3fa4e65ac2806955a4be8e969dd8a2636ccf Mon Sep 17 00:00:00 2001 From: Daniele Sluijters Date: Sat, 26 Nov 2022 12:09:55 +0100 Subject: [PATCH] Additional IP range validations (#1152) * [bugfix] Ensure requests happen over TCP It's possible for the network to be udp4 or udp6. This is rather unlikely to occur, but since we're given the network anyway as part of the Sanitize function getting called we might as well check for it. * [chore] Align reserved v6 blocks to IANA registry * [chore] Add test for ValidateIP The net and netip packages diverge in that net.ParseIP will consider an IPv4-mapped address to be an IPv4 address and as such it would get caught by the IPv4Reserved list. However, netip considers it an IPv6 address, so we need to ensure the mapped range is in IPv6Reserved. * [chore] Align reserved v4 blocks to IANA registry This includes a number of tests for /32's explicitly called out in the registry to ensure we always consider those invalid. --- internal/httpclient/client.go | 3 ++ internal/httpclient/sanitizer.go | 4 +++ internal/netutil/validate.go | 34 +++++++++++++++---- internal/netutil/validate_test.go | 54 +++++++++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 internal/netutil/validate_test.go diff --git a/internal/httpclient/client.go b/internal/httpclient/client.go index 95071534c..84076a97e 100644 --- a/internal/httpclient/client.go +++ b/internal/httpclient/client.go @@ -36,6 +36,9 @@ import ( // ErrInvalidRequest is returned if a given HTTP request is invalid and cannot be performed. var ErrInvalidRequest = errors.New("invalid http request") +// ErrInvalidNetwork is returned if the request would not be performed over TCP +var ErrInvalidNetwork = errors.New("invalid network type") + // ErrReservedAddr is returned if a dialed address resolves to an IP within a blocked or reserved net. var ErrReservedAddr = errors.New("dial within blocked / reserved IP range") diff --git a/internal/httpclient/sanitizer.go b/internal/httpclient/sanitizer.go index 6eef6898a..75c0b34b9 100644 --- a/internal/httpclient/sanitizer.go +++ b/internal/httpclient/sanitizer.go @@ -38,6 +38,10 @@ func (s *sanitizer) Sanitize(ntwrk, addr string, _ syscall.RawConn) error { return err } + if !(ntwrk == "tcp4" || ntwrk == "tcp6") { + return ErrInvalidNetwork + } + // Seperate the IP ip := ipport.Addr() diff --git a/internal/netutil/validate.go b/internal/netutil/validate.go index 2cb5aad0a..d6b99e70d 100644 --- a/internal/netutil/validate.go +++ b/internal/netutil/validate.go @@ -24,16 +24,34 @@ import ( var ( // IPv6Reserved contains IPv6 reserved IP prefixes. + // https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml IPv6Reserved = [...]netip.Prefix{ - netip.MustParsePrefix("::1/128"), // Loopback - netip.MustParsePrefix("fe80::/10"), // Link-local - netip.MustParsePrefix("fc00::/7"), // Unique Local - netip.MustParsePrefix("2001:db8::/32"), // Test, doc, examples - netip.MustParsePrefix("ff00::/8"), // Multicast - netip.MustParsePrefix("fec0::/10"), // Site-local, deprecated + netip.MustParsePrefix("::1/128"), // Loopback + netip.MustParsePrefix("::/128"), // Unspecified address + netip.MustParsePrefix("::ffff:0:0/96"), // IPv4-mapped address + netip.MustParsePrefix("64:ff9b::/96"), // IPv4/IPv6 translation, RFC 6052 + netip.MustParsePrefix("64:ff9b:1::/48"), // IPv4/IPv6 translation, RFC 8215 + netip.MustParsePrefix("100::/64"), // Discard prefix, RFC 6666 + netip.MustParsePrefix("2001::/23"), // IETF Protocol Assignments, RFC 2928 + netip.MustParsePrefix("2001::/32"), // Teredo + netip.MustParsePrefix("2001:1::1/128"), // Port Control Protocol Anycast, RFC 7723 + netip.MustParsePrefix("2001:1::2/128"), // Traversal Using Relays around NAT Anycast, RFC 8155 + netip.MustParsePrefix("2001:2::/48"), // Benchmarking, RFC 5180 + netip.MustParsePrefix("2001:3::/32"), // AMT, RFC 7450 + netip.MustParsePrefix("2001:4:112::/48"), // AS112-v6, RFC 7535 + netip.MustParsePrefix("2001:10::/28"), // ORCHID, deprecated + netip.MustParsePrefix("2001:20::/28"), // ORCHIDv2 + netip.MustParsePrefix("2001:db8::/32"), // Test, doc, examples + netip.MustParsePrefix("2002::/16"), // 6to4 + netip.MustParsePrefix("2620:4f:8000::/48"), // Direct Delegation AS112 Service, RFC 7534 + netip.MustParsePrefix("fc00::/7"), // Unique Local + netip.MustParsePrefix("fe80::/10"), // Link-local + netip.MustParsePrefix("fec0::/10"), // Site-local, deprecated + netip.MustParsePrefix("ff00::/8"), // Multicast } // IPv4Reserved contains IPv4 reserved IP prefixes. + // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml IPv4Reserved = [...]netip.Prefix{ netip.MustParsePrefix("0.0.0.0/8"), // Current network netip.MustParsePrefix("10.0.0.0/8"), // Private @@ -42,9 +60,13 @@ var ( netip.MustParsePrefix("169.254.0.0/16"), // Link-local netip.MustParsePrefix("172.16.0.0/12"), // Private netip.MustParsePrefix("192.0.0.0/24"), // RFC6890 + netip.MustParsePrefix("192.0.0.0/29"), // IPv4 Service Continuity Prefix, RFC 7335 netip.MustParsePrefix("192.0.2.0/24"), // Test, doc, examples + netip.MustParsePrefix("192.31.196.0/24"), // AS112-v4, RFC 7535 + netip.MustParsePrefix("192.52.193.0/24"), // AMT, RFC 7450 netip.MustParsePrefix("192.88.99.0/24"), // IPv6 to IPv4 relay netip.MustParsePrefix("192.168.0.0/16"), // Private + netip.MustParsePrefix("192.175.48.0/24"), // Direct Delegation AS112 Service, RFC 7534 netip.MustParsePrefix("198.18.0.0/15"), // Benchmarking tests netip.MustParsePrefix("198.51.100.0/24"), // Test, doc, examples netip.MustParsePrefix("203.0.113.0/24"), // Test, doc, examples diff --git a/internal/netutil/validate_test.go b/internal/netutil/validate_test.go new file mode 100644 index 000000000..37def4ce6 --- /dev/null +++ b/internal/netutil/validate_test.go @@ -0,0 +1,54 @@ +package netutil + +import ( + "net/netip" + "testing" +) + +func TestValidateIP(t *testing.T) { + tests := []struct { + name string + ip netip.Addr + }{ + // IPv4 tests + { + name: "IPv4 this host on this network", + ip: netip.MustParseAddr("0.0.0.0"), + }, + { + name: "IPv4 dummy address", + ip: netip.MustParseAddr("192.0.0.8"), + }, + { + name: "IPv4 Port Control Protocol Anycast", + ip: netip.MustParseAddr("192.0.0.9"), + }, + { + name: "IPv4 Traversal Using Relays around NAT Anycast", + ip: netip.MustParseAddr("192.0.0.10"), + }, + { + name: "IPv4 NAT64/DNS64 Discovery 1", + ip: netip.MustParseAddr("192.0.0.17"), + }, + { + name: "IPv4 NAT64/DNS64 Discovery 2", + ip: netip.MustParseAddr("192.0.0.171"), + }, + // IPv6 tests + { + name: "IPv4-mapped address", + ip: netip.MustParseAddr("::ffff:169.254.169.254"), + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if valid := ValidateIP(tc.ip); valid != false { + t.Fatalf("Expected IP %s to be: %t, got: %t", tc.ip, false, valid) + } + }) + } +}