Browser Security Best Practices

Chapter 2 · Practice 1 of 3

Use a strict, nonce-based CSP instead of a domain allowlist

From Content Security Policy

Why it matters

Older CSPs listed trusted domains: script-src 'self' cdn.example.com analytics.example.net. Those are easy to bypass when any allowed domain hosts user content or an old library with an injectable endpoint. OWASP's CSP Cheat Sheet recommends a strict policy based on nonces:

Content-Security-Policy: script-src 'nonce-{RANDOM}' 'strict-dynamic'; object-src 'none'; base-uri 'none';
DirectiveWhat it does
'nonce-{RANDOM}'only script tags carrying this response's nonce run
'strict-dynamic'scripts loaded by a trusted script are trusted too, so bundlers and loaders keep working
object-src 'none'blocks plugin content
base-uri 'none'stops an injected <base> tag redirecting relative script URLs

Nonce rules from OWASP:

  • generate a new nonce for every response, from a secure random generator
  • add it in your templating, on the script tags you wrote
  • never use middleware that rewrites every script tag to add a nonce, because injected tags would get one too

Many frameworks support this directly, for example Next.js reading a nonce from a request header set in middleware.

Example

TypeScript
import { randomBytes } from 'node:crypto';

export function cspHeaders() {
  const nonce = randomBytes(16).toString('base64'); // new for every response
  const csp = [
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    "object-src 'none'",
    "base-uri 'none'",
    "frame-ancestors 'none'",
  ].join('; ');
  return { nonce, header: csp }; // pass nonce to the template's script tags
}