From b1400abb5ce7d19d66e0bf51600088db058d2d3e Mon Sep 17 00:00:00 2001 From: sanjibani <18418553+sanjibani@users.noreply.github.com> Date: Thu, 25 Jun 2026 22:41:36 +0530 Subject: [PATCH] fix(csp): warn when useNonce fallback to unsafe-inline happens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A route handler that returns ctx.html(), ctx.json(), or ctx.text() instead of ctx.render() never sets the nonce on the response, so the CSP middleware silently falls back to the default directives with 'unsafe-inline' in script-src / style-src. The user opted into useNonce to lock the policy down — getting 'unsafe-inline' instead is a real security regression and they have no way to tell. Emit a console.warn with the route path so the developer knows the handler needs to switch to ctx.render(). Two new tests cover the warning in both the fallback and no-fallback cases. --- packages/fresh/src/middlewares/csp.ts | 11 +++++ packages/fresh/src/middlewares/csp_test.tsx | 54 +++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/packages/fresh/src/middlewares/csp.ts b/packages/fresh/src/middlewares/csp.ts index 0f0b6f558c4..0a1719b46b4 100644 --- a/packages/fresh/src/middlewares/csp.ts +++ b/packages/fresh/src/middlewares/csp.ts @@ -129,6 +129,17 @@ export function csp(options: CSPOptions = {}): Middleware { return d; }); } else { + // The route returned a response without going through ctx.render(), + // so Fresh never attached a nonce. The merged directives still + // contain 'unsafe-inline' for script-src / style-src and the user's + // intent (useNonce: true) was a locked-down policy — warn loudly so + // they know the handler needs to switch to ctx.render(). + // deno-lint-ignore no-console + console.warn( + `🍋 [fresh] csp middleware: useNonce is true but the response ${ + ctx.url.pathname + } has no nonce (handler likely returned ctx.html/ctx.json/ctx.text instead of ctx.render). Falling back to 'unsafe-inline'.`, + ); directives = merged; } diff --git a/packages/fresh/src/middlewares/csp_test.tsx b/packages/fresh/src/middlewares/csp_test.tsx index 3fc50f0fd4c..e9b53e465c5 100644 --- a/packages/fresh/src/middlewares/csp_test.tsx +++ b/packages/fresh/src/middlewares/csp_test.tsx @@ -165,6 +165,60 @@ Deno.test("CSP - useNonce with non-rendered response falls back to unsafe-inline expect(cspHeader).toContain("'unsafe-inline'"); }); +Deno.test("CSP - useNonce with non-rendered response warns the developer", async () => { + const warnings: string[] = []; + const original = console.warn; + // deno-lint-ignore no-console + console.warn = (msg: string) => { + warnings.push(String(msg)); + }; + try { + const app = new App() + .use(csp({ useNonce: true })) + .get("/api", () => new Response(JSON.stringify({ ok: true }))); + const server = new FakeServer(app.handler()); + const res = await server.get("/api"); + await res.body?.cancel(); + } finally { + // deno-lint-ignore no-console + console.warn = original; + } + + expect(warnings).toHaveLength(1); + expect(warnings[0]).toContain("/api"); + expect(warnings[0]).toContain("useNonce is true"); + expect(warnings[0]).toContain("unsafe-inline"); +}); + +Deno.test("CSP - useNonce with rendered response does not warn", async () => { + const warnings: string[] = []; + const original = console.warn; + // deno-lint-ignore no-console + console.warn = (msg: string) => { + warnings.push(String(msg)); + }; + try { + const app = new App() + .use(csp({ useNonce: true })) + .get("/", (ctx) => { + return ctx.render( + + + hello + , + ); + }); + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + await res.body?.cancel(); + } finally { + // deno-lint-ignore no-console + console.warn = original; + } + + expect(warnings).toHaveLength(0); +}); + Deno.test("CSP - useNonce generates unique nonce per request", async () => { const app = new App() .use(csp({ useNonce: true }))