From 12a0168271827650ddb0026d6277fbadf3dcf3ea Mon Sep 17 00:00:00 2001 From: Thanh_Dang Date: Wed, 24 Jun 2026 14:12:38 +0700 Subject: [PATCH] fix(security): block 198.18.0.0/15 and 240.0.0.0/4 in SSRF protection (#1269) Add RFC 2544 benchmarking range (198.18.0.0/15) and reserved range (240.0.0.0/4) to both SSRF blocklists (security package + web_fetch tool) to prevent internal network access via these special-use IPs. Closes #1218 --- internal/security/ssrf.go | 4 +++ internal/security/ssrf_test.go | 14 +++++++++ internal/tools/web_shared.go | 4 ++- internal/tools/web_shared_test.go | 47 +++++++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 internal/tools/web_shared_test.go diff --git a/internal/security/ssrf.go b/internal/security/ssrf.go index 98bd757d..5b145b35 100644 --- a/internal/security/ssrf.go +++ b/internal/security/ssrf.go @@ -52,6 +52,10 @@ func init() { "172.16.0.0/12", "192.168.0.0/16", "fc00::/7", + // Benchmarking (RFC 2544) + "198.18.0.0/15", + // Reserved for future use + "240.0.0.0/4", // Multicast "224.0.0.0/4", "ff00::/8", diff --git a/internal/security/ssrf_test.go b/internal/security/ssrf_test.go index c9cdf831..65adf82a 100644 --- a/internal/security/ssrf_test.go +++ b/internal/security/ssrf_test.go @@ -45,6 +45,20 @@ func TestValidate_RejectsRFC1918(t *testing.T) { } } +func TestValidate_RejectsBenchmarking(t *testing.T) { + _, _, err := Validate("http://198.18.1.1/") + if err == nil { + t.Fatal("expected error for benchmarking range (198.18.0.0/15), got nil") + } +} + +func TestValidate_RejectsReserved(t *testing.T) { + _, _, err := Validate("http://240.1.2.3/") + if err == nil { + t.Fatal("expected error for reserved range (240.0.0.0/4), got nil") + } +} + func TestValidate_RejectsMulticast(t *testing.T) { _, _, err := Validate("http://224.0.0.1/") if err == nil { diff --git a/internal/tools/web_shared.go b/internal/tools/web_shared.go index c524707d..313f2399 100644 --- a/internal/tools/web_shared.go +++ b/internal/tools/web_shared.go @@ -131,7 +131,9 @@ func isPrivateIP(ipStr string) bool { {"169.254.0.0", 16}, // link-local {"172.16.0.0", 12}, // private {"192.168.0.0", 16}, // private - {"100.64.0.0", 10}, // carrier-grade NAT + {"100.64.0.0", 10}, // carrier-grade NAT (RFC 6598) + {"198.18.0.0", 15}, // benchmarking (RFC 2544) + {"240.0.0.0", 4}, // reserved for future use } for _, r := range privateRanges { diff --git a/internal/tools/web_shared_test.go b/internal/tools/web_shared_test.go new file mode 100644 index 00000000..0faf4033 --- /dev/null +++ b/internal/tools/web_shared_test.go @@ -0,0 +1,47 @@ +package tools + +import "testing" + +func TestIsPrivateIP(t *testing.T) { + blocked := []struct { + ip string + desc string + }{ + {"10.0.0.1", "RFC 1918 class A"}, + {"172.16.5.1", "RFC 1918 class B"}, + {"192.168.1.1", "RFC 1918 class C"}, + {"127.0.0.1", "loopback"}, + {"169.254.169.254", "link-local / cloud metadata"}, + {"100.64.0.1", "carrier-grade NAT"}, + {"198.18.1.1", "benchmarking (RFC 2544)"}, + {"198.19.255.1", "benchmarking upper (RFC 2544)"}, + {"240.1.2.3", "reserved for future use"}, + {"255.255.255.255", "reserved broadcast"}, + {"::1", "IPv6 loopback"}, + {"fe80::1", "IPv6 link-local"}, + {"fc00::1", "IPv6 unique local"}, + } + for _, tc := range blocked { + t.Run(tc.desc, func(t *testing.T) { + if !isPrivateIP(tc.ip) { + t.Errorf("expected %s (%s) to be blocked", tc.ip, tc.desc) + } + }) + } + + allowed := []struct { + ip string + desc string + }{ + {"8.8.8.8", "Google DNS"}, + {"93.184.216.34", "example.com"}, + {"198.51.100.1", "public IP near benchmarking range"}, + } + for _, tc := range allowed { + t.Run(tc.desc, func(t *testing.T) { + if isPrivateIP(tc.ip) { + t.Errorf("expected %s (%s) to be allowed", tc.ip, tc.desc) + } + }) + } +}