From 7ed50824b6442a9da585ae013c60fc0e23b15e7b Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 26 Apr 2026 10:14:47 -0700 Subject: [PATCH] fix(platform/ssrf): allow RFC-1918 in MOLECULE_ENV=development MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docker-compose dev pattern puts platform and workspace containers on the same docker bridge network (172.18.0.0/16, RFC-1918). The runtime registers via its docker-internal hostname which DNS-resolves to a 172.18.x.x IP. The SSRF defence's isPrivateOrMetadataIP rejected those, so every workspace POST through the platform proxy returned 'workspace URL is not publicly routable' — breaking the entire docker- compose dev loop. Fix: in isPrivateOrMetadataIP, treat MOLECULE_ENV=development the same as SaaS mode for RFC-1918 relaxation. Both share the 'trusted intra- network routing' property — SaaS is sibling EC2s in the same VPC, dev is sibling containers on the same docker bridge. Always-blocked categories (metadata link-local, TEST-NET, CGNAT) stay blocked. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- workspace-server/internal/handlers/ssrf.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/workspace-server/internal/handlers/ssrf.go b/workspace-server/internal/handlers/ssrf.go index a84426f1..2e795e90 100644 --- a/workspace-server/internal/handlers/ssrf.go +++ b/workspace-server/internal/handlers/ssrf.go @@ -127,7 +127,16 @@ var testAllowLoopback = false // container deployments the relaxation is off and every private range // stays blocked. func isPrivateOrMetadataIP(ip net.IP) bool { - saas := saasMode() + // MOLECULE_ENV=development is the dev-host pattern: platform and + // workspace containers share a docker bridge network (172.18.0.0/16, + // RFC-1918). Treat that the same as SaaS for private-range relaxation + // — both share the "trusted intra-network routing" property. Without + // this, every workspace registration via docker-internal hostname + // resolves to 172.18.x.x and gets rejected as + // "workspace URL is not publicly routable", breaking the entire + // docker-compose dev loop. Always-blocked categories (metadata link- + // local, TEST-NET, CGNAT) remain blocked regardless. + saas := saasMode() || devModeAllowsLoopback() // IPv4 path. if ip4 := ip.To4(); ip4 != nil {