[]previous and next page

Content Security Policy and nonces

security.csp turns on a Content-Security-Policy with a fresh nonce per request.

For agentsUpdated

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.

csp.tsts
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")}`);
Expected output
img-src 'self' https://images.example.com
script-src 'self' 'nonce-abc'
upgrade-insecure-requests
object-src is gone: true

The 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.

src/routes/api/nonce.tsts
import { json, type RequestEvent } from "@shadow-js/dusk/server";

export const GET = (event: RequestEvent) => json({ nonce: event.nonce ?? null });