Content Security Policy and nonces
security.csp turns on a Content-Security-Policy with a fresh nonce per request.
security.csp turns on a Content-Security-Policy with a fresh nonce per request. csp: {} is the strict default:
defaultCspDirectives() returns it, and your directives are merged over it by name.
| Directive | Default sources |
|---|---|
default-src, font-src, connect-src, base-uri, form-action, frame-ancestors |
'self' |
script-src, style-src |
'self' and the nonce |
style-src-attr |
'unsafe-inline' (style attributes in server markup) |
img-src |
'self' and data: |
object-src |
'none' |
CspOptions has four fields: directives (name to a list of sources; true writes a bare directive such as
upgrade-insecure-requests; an empty list removes a default directive), useDefaults (default true),
reportOnly (send Content-Security-Policy-Report-Only) and reportUri. The source "{nonce}"
(NONCE_TOKEN) becomes 'nonce-<value>' for the request. buildCsp(options, nonce) is the pure function; it
rejects a directive name or source that could add a directive (;, ,, whitespace, control characters) and a
header longer than MAX_CSP_LENGTH (8192), and the handler runs it at startup so a bad policy fails before the first
request.
import { buildCsp } from "@shadow-js/dusk/server";
const header = buildCsp(
{
directives: {
"img-src": ["'self'", "https://images.example.com"],
"upgrade-insecure-requests": true,
"object-src": [],
},
},
"abc",
);
const parts = header.split("; ");
console.log(parts.find((part) => part.startsWith("img-src")));
console.log(parts.find((part) => part.startsWith("script-src")));
console.log(parts.find((part) => part.startsWith("upgrade")));
console.log(`object-src is gone: ${!header.includes("object-src")}`);
img-src 'self' https://images.example.com
script-src 'self' 'nonce-abc'
upgrade-insecure-requests
object-src is gone: trueThe policy is added to HTML responses only (a JSON answer does not need one) and only when the response has no
policy of its own. The nonce is event.nonce in middleware, API routes and server functions. Dusk puts it on every
script it writes: the module script of the client entry, and the swap scripts of a streamed page (the router data
block is type="application/json", not executable, and has no nonce). Write the same value on any inline script you
add. With security.nonce: true a nonce exists without a policy, for an app that sets its own header. A third-party script that a page loads from another host needs that host in script-src, and its element needs the same nonce. Clerk is an example: in its default script mode, ClerkProvider loads Clerk's bundles from the frontend API host of the key, and the Clerk guide lists every host it needs.
import { json, type RequestEvent } from "@shadow-js/dusk/server";
export const GET = (event: RequestEvent) => json({ nonce: event.nonce ?? null });