From d5254cfca370304957e8066ed1f6a8691c89c801 Mon Sep 17 00:00:00 2001 From: ttbombadil Date: Sun, 4 Oct 2026 22:28:51 +0200 Subject: [PATCH] fix(security): preserve the complete Helmet CSP --- server/index.ts | 11 +--------- tests/server/dev-entrypoint.test.ts | 31 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/server/index.ts b/server/index.ts index afa17d232..a9da43101 100644 --- a/server/index.ts +++ b/server/index.ts @@ -83,11 +83,7 @@ if (config.trust.mode === "gateway") { app.set("trust proxy", config.trust.trustedProxy); } -// Security: Helmet adds various HTTP headers for protection -function getFrameAncestorsHeader(): string { - return `frame-ancestors ${parseAllowedFrameAncestors().join(" ")}`; -} - +// Security: Helmet owns the complete CSP, including the embedding policy. app.use( helmet({ frameguard: false, // Deactivate X-Frame-Options; we use CSP frame-ancestors instead @@ -107,11 +103,6 @@ app.use( }), ); -app.use((_, res, next) => { - res.setHeader("Content-Security-Policy", getFrameAncestorsHeader()); - next(); -}); - // Security: Rate limiting to prevent DoS attacks // In test/development mode, use higher limits const isTestMode = config.isTest || config.server.disableRateLimit; diff --git a/tests/server/dev-entrypoint.test.ts b/tests/server/dev-entrypoint.test.ts index 0e77e760d..7d40e46e9 100644 --- a/tests/server/dev-entrypoint.test.ts +++ b/tests/server/dev-entrypoint.test.ts @@ -148,6 +148,37 @@ async function waitForReadiness(child: ChildProcess, port: number, output: () => } describe("local development entrypoint", () => { + it("preserves the complete CSP in final HTTP responses with configured frame ancestors", async () => { + const port = await reservePort(); + const env = createLocalServerEnv(port); + env.SIMULATOR_ALLOWED_PARENT_ORIGINS = "'self',https://course.example,https://course.example"; + env.UNOSIM_EXAMPLES_SOURCE = ""; + const child = spawnLocalDevelopment(env); + const output = collectOutput(child); + try { + await waitForReadiness(child, port, output); + for (const route of ["/api/readiness", "/examples"]) { + const response = await fetch(`http://127.0.0.1:${port}${route}`); + const policy = response.headers.get("content-security-policy"); + expect(policy).not.toBeNull(); + const directives = new Map(policy!.split(";").map((directive) => { + const [name, ...values] = directive.trim().split(/\s+/); + return [name, values]; + })); + expect(directives.get("default-src")).toEqual(["'self'"]); + expect(directives.get("script-src")).toContain("'self'"); + expect(directives.get("connect-src")).toEqual([ + "'self'", "ws:", "wss:", "https://fonts.googleapis.com", "https://fonts.gstatic.com", + ]); + expect(directives.get("worker-src")).toEqual(["'self'", "blob:", "data:"]); + expect(directives.get("frame-ancestors")).toEqual(["'self'", "https://course.example"]); + expect(response.headers.get("x-frame-options")).toBeNull(); + } + } finally { + await stopProcess(child); + } + }, 20_000); + it("ignores inherited removed runtime selectors while preserving local startup and capacity overrides", async () => { const port = await reservePort(); const env = createLocalServerEnv(port);