From 0a9d7af2bf48c71eecdb87bb3493a01efa3afc33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 26 Mar 2026 18:11:02 +0100 Subject: [PATCH 1/5] feat: add nonce support for inline style and script tags in CSP - Auto-inject nonce attribute onto inline + + , + ); + }); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const html = await res.text(); + const cspHeader = res.headers.get("Content-Security-Policy")!; + + const nonceMatch = cspHeader.match(/nonce-([a-f0-9]+)/); + expect(nonceMatch).not.toBeNull(); + const nonce = nonceMatch![1]; + + // Inline script should have the nonce + expect(html).toContain(`nonce="${nonce}"`); +}); + +Deno.test("CSP - useNonce with non-rendered response falls back to unsafe-inline", async () => { + 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(); + const cspHeader = res.headers.get("Content-Security-Policy")!; + + // Non-rendered response has no nonce, so unsafe-inline stays + expect(cspHeader).toContain("'unsafe-inline'"); +}); + +Deno.test("CSP - useNonce generates unique nonce per request", async () => { + const app = new App() + .use(csp({ useNonce: true })) + .get("/", (ctx) => { + return ctx.render( + + + hello + , + ); + }); + + const server = new FakeServer(app.handler()); + const res1 = await server.get("/"); + await res1.body?.cancel(); + const res2 = await server.get("/"); + await res2.body?.cancel(); + + const nonce1 = res1.headers.get("Content-Security-Policy")!.match( + /nonce-([a-f0-9]+)/, + )![1]; + const nonce2 = res2.headers.get("Content-Security-Policy")!.match( + /nonce-([a-f0-9]+)/, + )![1]; + + expect(nonce1).not.toEqual(nonce2); +}); + +Deno.test("CSP - existing nonce on tag is preserved", async () => { + 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("/"); + const html = await res.text(); + + // The explicit nonce should be preserved, not overwritten + expect(html).toContain('nonce="custom-nonce"'); +}); diff --git a/packages/fresh/src/runtime/server/preact_hooks.ts b/packages/fresh/src/runtime/server/preact_hooks.ts index 320439bb97e..77213206904 100644 --- a/packages/fresh/src/runtime/server/preact_hooks.ts +++ b/packages/fresh/src/runtime/server/preact_hooks.ts @@ -132,6 +132,18 @@ options[OptionsType.VNODE] = (vnode) => { ); } } else if (typeof vnode.type === "string") { + // Auto-inject nonce onto inline script/style tags + if ( + RENDER_STATE !== null && + (vnode.type === "script" || vnode.type === "style") + ) { + // deno-lint-ignore no-explicit-any + const props = vnode.props as any; + if (!props.nonce) { + props.nonce = RENDER_STATE.nonce; + } + } + if (vnode.type === "body") { const scripts = h(FreshScripts, null); if (vnode.props.children == null) { From 1ccd7d326d06939b97f130a230cc29bb912c5126 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Thu, 26 Mar 2026 19:52:25 +0100 Subject: [PATCH 2/5] docs: document nonce-based CSP Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/latest/plugins/csp.md | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/docs/latest/plugins/csp.md b/docs/latest/plugins/csp.md index 5ef1b6e5dc9..a904c89621c 100644 --- a/docs/latest/plugins/csp.md +++ b/docs/latest/plugins/csp.md @@ -25,6 +25,43 @@ const app = new App() .get("/", () => new Response("hello")); ``` +## Nonce-based CSP + +For stricter security, you can use nonce-based CSP instead of +`'unsafe-inline'`. This ensures only inline `