Security: Include CGNAT (Carrier-Grade NAT) in internal IP checks (GHSA-j3fj-qppj-fmmc)

CGNAT (Carrier-Grade NAT) is a technique used by ISPs to conserve IPv4 addresses. Instead of assigning a unique public IP to every customer, the ISP places many customers behind a shared NAT, then gives them all addresses from the reserved 100.64.0.0/10 range (RFC 6598) on their internal network.

This means traffic from multiple customers exits through a small pool of public IPs - a second layer of NAT on top of whatever NAT the customer's own router does (hence "double NAT").
This commit is contained in:
Ralph Slooten
2026-05-09 14:43:55 +12:00
parent 136bdde953
commit bcd1bc71ee
2 changed files with 73 additions and 1 deletions

View File

@@ -5,6 +5,15 @@ import (
"net/url"
)
// cgnatRange is the CGNAT shared address space (RFC 6598), not covered by net.IP.IsPrivate().
// CGNAT (Carrier-Grade NAT) is a technique used by ISPs to conserve IPv4 addresses. Instead of assigning a unique
// public IP to every customer, the ISP places many customers behind a shared NAT, then gives them all addresses
// from the reserved 100.64.0.0/10 range (RFC 6598) on their internal network.
var cgnatRange = func() *net.IPNet {
_, cidr, _ := net.ParseCIDR("100.64.0.0/10")
return cidr
}()
// IsInternalIP checks if the given IP address is an internal IP address (e.g., loopback, private, link-local, or multicast).
// IsLoopback — 127.0.0.0/8, ::1
// IsPrivate — 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7
@@ -12,13 +21,15 @@ import (
// IsLinkLocalMulticast — 224.0.0.0/24, ff02::/16
// IsUnspecified — 0.0.0.0, ::
// IsMulticast — 224.0.0.0/4, ff00::/8
// CGNAT — 100.64.0.0/10 (RFC 6598) (Carrier-Grade NAT)
func IsInternalIP(ip net.IP) bool {
return ip.IsLoopback() ||
ip.IsPrivate() ||
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() ||
ip.IsUnspecified() ||
ip.IsMulticast()
ip.IsMulticast() ||
cgnatRange.Contains(ip)
}
// IsValidLinkURL checks if the provided string is a valid URL with http or https scheme and a non-empty hostname.

View File

@@ -1,10 +1,71 @@
package tools
import (
"net"
"reflect"
"testing"
)
func TestIsInternalIP(t *testing.T) {
internal := []string{
"127.0.0.1", // loopback
"::1", // IPv6 loopback
"10.0.0.1", // private
"172.16.0.1", // private
"192.168.1.1", // private
"169.254.1.1", // link-local unicast
"fe80::1", // IPv6 link-local
"0.0.0.0", // unspecified
"224.0.0.1", // multicast
"100.64.0.1", // CGNAT start
"100.127.255.255", // CGNAT end
}
external := []string{
"8.8.8.8",
"1.1.1.1",
"100.128.0.1", // just outside CGNAT range
}
for _, s := range internal {
ip := net.ParseIP(s)
if !IsInternalIP(ip) {
t.Errorf("expected %s to be internal", s)
}
}
for _, s := range external {
ip := net.ParseIP(s)
if IsInternalIP(ip) {
t.Errorf("expected %s to be external", s)
}
}
}
func TestIsValidLinkURL(t *testing.T) {
valid := []string{
"http://example.com",
"https://example.com",
"https://example.com/path?q=1#anchor",
}
invalid := []string{
"",
"ftp://example.com",
"example.com",
"//example.com",
"https://",
}
for _, s := range valid {
if !IsValidLinkURL(s) {
t.Errorf("expected %q to be a valid link URL", s)
}
}
for _, s := range invalid {
if IsValidLinkURL(s) {
t.Errorf("expected %q to be an invalid link URL", s)
}
}
}
func TestArgsParser(t *testing.T) {
tests := map[string][]string{}
tests["this is a test"] = []string{"this", "is", "a", "test"}