fix(platform/ssrf): allow RFC-1918 in MOLECULE_ENV=development

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)
This commit is contained in:
Hongming Wang 2026-04-26 10:14:47 -07:00
parent d97d7d4768
commit 7ed50824b6

View File

@ -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 {