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 }))