diff --git a/packages/shared/src/mcp-apps/csp-injector.test.ts b/packages/shared/src/mcp-apps/csp-injector.test.ts index 76a5ed1fc8..f2291ddcbd 100644 --- a/packages/shared/src/mcp-apps/csp-injector.test.ts +++ b/packages/shared/src/mcp-apps/csp-injector.test.ts @@ -123,6 +123,17 @@ describe("injectCSP with resourceCsp", () => { ); }); + it("authorizes the WebSocket scheme for each connect domain", () => { + // CSP does not derive wss: from an https: source, so the injector adds it. + const result = injectCSP(html, { + resourceCsp: { + connectDomains: ["https://api.example.com", "http://local.test:3001"], + }, + }); + expect(result).toContain("wss://api.example.com"); + expect(result).toContain("ws://local.test:3001"); + }); + it("adds frameDomains to frame-src", () => { const result = injectCSP(html, { resourceCsp: { diff --git a/packages/shared/src/mcp-apps/csp-injector.ts b/packages/shared/src/mcp-apps/csp-injector.ts index 037c3bce01..269f70ebf1 100644 --- a/packages/shared/src/mcp-apps/csp-injector.ts +++ b/packages/shared/src/mcp-apps/csp-injector.ts @@ -73,6 +73,17 @@ function validateDomains(domains: string[] | undefined): string[] { return domains.filter((d) => DOMAIN_RE.test(d)); } +/** + * CSP `connect-src` does not treat an `https:` source as covering `wss:` (a + * WebSocket to a host listed only as `https://host` is blocked — verified in + * Chrome). So for each connect domain also authorize its WebSocket scheme: + * `https://host` → `wss://host`, `http://host` → `ws://host`. Originals are kept + * first so plain fetch/XHR still match and existing output is unchanged up front. + */ +function withWebSocketSchemes(domains: string[]): string[] { + return [...domains, ...domains.map((d) => d.replace(/^http/, "ws"))]; +} + function buildCSPPolicy(options: CSPInjectorOptions): string { if (options.csp) return options.csp; @@ -116,7 +127,7 @@ function buildCSPPolicy(options: CSPInjectorOptions): string { : "media-src * data: blob:", hasResourceDomains ? `font-src data: ${rd}` : "font-src data:", hasConnectDomains - ? `connect-src ${connectDomains.join(" ")}` + ? `connect-src ${withWebSocketSchemes(connectDomains).join(" ")}` : "connect-src 'none'", hasFrameDomains ? `frame-src ${frameDomains.join(" ")}`