Skip to content

Commit cb2d986

Browse files
committed
feat: add insecureUnsafeInline option to CSP middleware
1 parent 7e4a82f commit cb2d986

2 files changed

Lines changed: 66 additions & 0 deletions

File tree

packages/fresh/src/middlewares/csp.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@ export interface CSPOptions {
1919
* allow those Fresh-rendered inline elements.
2020
*/
2121
useNonce?: boolean;
22+
23+
/**
24+
* If true and `useNonce` is also true, keeps `'unsafe-inline'`
25+
* alongside the nonce in the CSP header instead of removing it.
26+
* This is useful when third-party inline scripts or styles (e.g.
27+
* analytics, reporting widgets) also need to execute on the page.
28+
*/
29+
insecureUnsafeInline?: boolean;
2230
}
2331

2432
/**
@@ -64,6 +72,7 @@ export function csp<State>(options: CSPOptions = {}): Middleware<State> {
6472
reportTo,
6573
csp = [],
6674
useNonce = false,
75+
insecureUnsafeInline = false,
6776
} = options;
6877

6978
const defaultCsp = [
@@ -124,6 +133,12 @@ export function csp<State>(options: CSPOptions = {}): Middleware<State> {
124133
const spaceIdx = d.indexOf(" ");
125134
const name = spaceIdx === -1 ? d : d.slice(0, spaceIdx);
126135
if (INLINE_DIRECTIVES.has(name) && d.includes("'unsafe-inline'")) {
136+
if (insecureUnsafeInline) {
137+
return d.replaceAll(
138+
"'unsafe-inline'",
139+
`'unsafe-inline' 'nonce-${nonce}'`,
140+
);
141+
}
127142
return d.replaceAll("'unsafe-inline'", `'nonce-${nonce}'`);
128143
}
129144
return d;

packages/fresh/src/middlewares/csp_test.tsx

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,3 +260,54 @@ Deno.test("CSP - useNonce replaces unsafe-inline in default-src", async () => {
260260
// default-src should have nonce, not unsafe-inline
261261
expect(cspHeader).toMatch(/default-src 'self' 'nonce-[a-f0-9]+'/);
262262
});
263+
264+
Deno.test("CSP - useNonce with insecureUnsafeInline keeps both", async () => {
265+
const app = new App()
266+
.use(csp({ useNonce: true, insecureUnsafeInline: true }))
267+
.get("/", (ctx) => {
268+
return ctx.render(
269+
<html>
270+
<head>
271+
<style>{"body { color: red; }"}</style>
272+
</head>
273+
<body>
274+
<h1>hello</h1>
275+
</body>
276+
</html>,
277+
);
278+
});
279+
280+
const server = new FakeServer(app.handler());
281+
const res = await server.get("/");
282+
const html = await res.text();
283+
const cspHeader = res.headers.get("Content-Security-Policy")!;
284+
285+
// Should contain both unsafe-inline and nonce
286+
expect(cspHeader).toContain("'unsafe-inline'");
287+
expect(cspHeader).toMatch(
288+
/script-src 'self' 'unsafe-inline' 'nonce-[a-f0-9]+'/,
289+
);
290+
expect(cspHeader).toMatch(
291+
/style-src 'self' 'unsafe-inline' 'nonce-[a-f0-9]+'/,
292+
);
293+
294+
// HTML should still have nonce on the style tag
295+
const nonceMatch = cspHeader.match(/nonce-([a-f0-9]+)/);
296+
expect(nonceMatch).not.toBeNull();
297+
const nonce = nonceMatch![1];
298+
expect(html).toContain(`nonce="${nonce}"`);
299+
});
300+
301+
Deno.test("CSP - insecureUnsafeInline without useNonce has no effect", async () => {
302+
const handler = new App()
303+
.use(csp({ insecureUnsafeInline: true }))
304+
.get("/", () => new Response("ok"))
305+
.handler();
306+
307+
const res = await handler(new Request("https://localhost/"));
308+
const cspHeader = res.headers.get("Content-Security-Policy")!;
309+
310+
// Without useNonce, insecureUnsafeInline should not change anything
311+
expect(cspHeader).toContain("'unsafe-inline'");
312+
expect(cspHeader).not.toContain("'nonce-");
313+
});

0 commit comments

Comments
 (0)