From ab7aadd7fa8daaa31792680d7e91fa1b34510caf Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Sun, 7 Jun 2026 22:53:21 -0700 Subject: [PATCH] fix(registry): case-fold + trim-dot in isPlatformTunnelHostname (#2425 follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security review of #2425 found two availability bugs: net/url Hostname() does NOT lowercase and keeps a trailing dot, so a legitimate WS-...MOLECULESAI.APP or FQDN-form ws-x.moleculesai.app. would fail the case-sensitive prefix/suffix match and get blocked at register — the exact failure the pending-tunnel allowance exists to cure. Fold case + trim the trailing dot before matching (DNS is case-insensitive; trailing dot is the same name). Also lowercases MOLECULE_APP_DOMAIN. Fails closed unchanged for non-platform hosts. Tests add the WS-/uppercase, trailing-dot, and parent-domain-trick cases. Co-Authored-By: Claude Opus 4.8 (1M context) --- workspace-server/internal/handlers/registry.go | 8 ++++++-- workspace-server/internal/handlers/registry_test.go | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/handlers/registry.go b/workspace-server/internal/handlers/registry.go index e3ed11330..6c286ecd5 100644 --- a/workspace-server/internal/handlers/registry.go +++ b/workspace-server/internal/handlers/registry.go @@ -309,12 +309,16 @@ func validateAgentURL(rawURL string) error { // (covers prod `*.moleculesai.app` and staging `*.staging.moleculesai.app`) and // is overridable via MOLECULE_APP_DOMAIN for other deployments. func isPlatformTunnelHostname(h string) bool { - // DNS is case-insensitive and FQDN-form hostnames may carry a trailing dot. + // Normalize: net/url's Hostname() does NOT lowercase and keeps a trailing dot, + // so a legitimate `WS-…MOLECULESAI.APP` or FQDN-form `ws-x.moleculesai.app.` + // would otherwise fail this case-sensitive match and get blocked (the exact + // availability bug this allowance exists to cure). DNS is case-insensitive and + // the trailing dot is the same name, so fold both before comparing. h = strings.ToLower(strings.TrimSuffix(h, ".")) if !strings.HasPrefix(h, "ws-") { return false } - domain := strings.TrimSpace(os.Getenv("MOLECULE_APP_DOMAIN")) + domain := strings.ToLower(strings.TrimSpace(os.Getenv("MOLECULE_APP_DOMAIN"))) if domain == "" { domain = "moleculesai.app" } diff --git a/workspace-server/internal/handlers/registry_test.go b/workspace-server/internal/handlers/registry_test.go index 1f8bff070..a7927adb7 100644 --- a/workspace-server/internal/handlers/registry_test.go +++ b/workspace-server/internal/handlers/registry_test.go @@ -985,6 +985,7 @@ func TestValidateAgentURL_PendingPlatformTunnel(t *testing.T) { {"api.moleculesai.app", false}, // no ws- prefix {"ws-x.fakemoleculesai.app", false}, // lookalike domain, not a subdomain {"ws-abc123moleculesai.app", false}, // missing dot before platform domain + {"ws-x.moleculesai.app.attacker.com", false}, // parent-domain trick } { if got := isPlatformTunnelHostname(tc.h); got != tc.want { t.Errorf("isPlatformTunnelHostname(%q)=%v want %v", tc.h, got, tc.want) -- 2.52.0