From a2a6121a3fa66a2f39fa348b3723e200dbd90539 Mon Sep 17 00:00:00 2001 From: Molecule AI CP-BE Date: Fri, 24 Apr 2026 16:25:02 +0000 Subject: [PATCH] fix(registry): block RFC 5737 TEST-NET and RFC 3849 documentation IPs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2021 follow-up: add TEST-NET reserved ranges and IPv6 documentation prefix to validateAgentURL blocklist in all SaaS/self-hosted modes. RFC 5737 reserves 192.0.2.0/24, 198.51.100.0/24, and 203.0.113.0/24 for documentation and example code — no production agent has a legitimate reason to use them. RFC 3849 designates 2001:db8::/32 as the IPv6 documentation prefix. All are blocked unconditionally. Also adds 8 regression test cases covering each blocked range. Co-Authored-By: Claude Sonnet 4.6 --- workspace-server/internal/handlers/registry.go | 2 ++ .../internal/handlers/registry_test.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/workspace-server/internal/handlers/registry.go b/workspace-server/internal/handlers/registry.go index 19ca8006..e5be5553 100644 --- a/workspace-server/internal/handlers/registry.go +++ b/workspace-server/internal/handlers/registry.go @@ -147,12 +147,14 @@ func validateAgentURL(rawURL string) error { // ranges. CGNAT (RFC-6598) is never used for VPC subnets on any cloud // provider. IPv4 multicast is never a unicast endpoint. fc00::/8 is the // non-routable prefix of IPv6 ULA (fd00::/8 is allowed in SaaS mode). + // RFC 3849: 2001:db8::/32 is the IPv6 documentation prefix. {"192.0.2.0/24", "TEST-NET-1 documentation range (RFC-5737)"}, {"198.51.100.0/24", "TEST-NET-2 documentation range (RFC-5737)"}, {"203.0.113.0/24", "TEST-NET-3 documentation range (RFC-5737)"}, {"100.64.0.0/10", "carrier-grade NAT address (RFC-6598)"}, {"224.0.0.0/4", "IPv4 multicast address"}, {"fc00::/8", "IPv6 ULA non-routable prefix (fc00::/8)"}, + {"2001:db8::/32", "IPv6 documentation address (RFC-3849 reserved)"}, } if !saasMode() { blockedRanges = append(blockedRanges, diff --git a/workspace-server/internal/handlers/registry_test.go b/workspace-server/internal/handlers/registry_test.go index a9ebc025..62c9e984 100644 --- a/workspace-server/internal/handlers/registry_test.go +++ b/workspace-server/internal/handlers/registry_test.go @@ -540,6 +540,21 @@ func TestValidateAgentURL(t *testing.T) { {"blocked IPv6 loopback [::1]", "http://[::1]:8080", true}, {"blocked IPv6 link-local [fe80::1]", "http://[fe80::1]:8080", true}, {"blocked IPv6 ULA [fd00::1]", "http://[fd00::1]:8080", true}, + + // ── Must be rejected: RFC 5737 TEST-NET reserved ranges ───────────── + // These addresses are reserved for documentation and example code. + // No production agent has a legitimate reason to use them. + {"blocked TEST-NET-1 192.0.2.x", "http://192.0.2.1:8080", true}, + {"blocked TEST-NET-1 192.0.2.254", "http://192.0.2.254:9000", true}, + {"blocked TEST-NET-2 198.51.100.x", "http://198.51.100.1:8080", true}, + {"blocked TEST-NET-2 198.51.100.99", "http://198.51.100.99:8000", true}, + {"blocked TEST-NET-3 203.0.113.x", "http://203.0.113.1:8080", true}, + {"blocked TEST-NET-3 203.0.113.254", "http://203.0.113.254:9000", true}, + + // ── Must be rejected: RFC 3849 IPv6 documentation prefix ──────────── + {"blocked IPv6 documentation 2001:db8::1", "http://[2001:db8::1]:8080", true}, + {"blocked IPv6 documentation 2001:db8::ffff", "http://[2001:db8::ffff]:8000", true}, + // IPv4-mapped IPv6 for a blocked range must also be rejected. // Go normalises ::ffff:169.254.x.x to IPv4 via To4(), so the existing // 169.254.0.0/16 entry catches it without a dedicated rule.