Learn · Strict CSP
Nonces, hashes, and allowlists
Three ways to allow inline and dynamically loaded scripts under a Content Security Policy — and how to pick the right one for your application.
Why this choice matters
The single biggest CSP decision is how you handle inline scripts. The naive answer — 'unsafe-inline'— defeats the entire policy: it's the directive equivalent of leaving the front door open while bolting every window shut.
Modern guidance from Google's web.dev team and the W3C is to use either a per-response nonce or a per-block hash, combined with 'strict-dynamic' so trusted scripts can transitively load other scripts. The question is which mechanism fits your delivery model.
The three approaches
Most production policies end up combining elements. A typical strict CSP uses a nonce for first-party inline scripts plus hostname allowlists for a small number of well-vetted vendors.
Allowlist (host-based)
When to reach for it: Easy to deploy on legacy applications. Lets you add CSP without changing how the application emits markup.
How it looks: Each directive lists explicit hostnames or scheme sources, e.g. script-src 'self' https://cdn.example.com https://www.googletagmanager.com.
Tradeoffs: Brittle and frequently bypassable — if any allowed origin hosts a JSONP endpoint or a vulnerable script, attackers can route around the policy. Modern guidance treats pure allowlists as a starting point, not the destination.
Nonce-based
When to reach for it: Right for HTML pages that are rendered server-side or at the edge — anywhere you can inject a fresh random value per response.
How it looks: Generate a cryptographically random value per request, set it on the response header (script-src 'nonce-{value}') and on every inline <script nonce="{value}">. Pair with 'strict-dynamic' so trusted scripts can load further trusted scripts.
Tradeoffs: Requires per-response control over both header and HTML. Cached HTML breaks unless you cache the nonce alongside the body, or vary on a fresh value.
Hash-based
When to reach for it: Right for static or fully cacheable HTML — single-page apps, marketing pages, generated sites — where the inline content doesn't change per request.
How it looks: Compute a sha256/sha384/sha512 hash of each inline script or style block and add 'sha256-{hash}' to the corresponding directive.
Tradeoffs: Tedious to maintain by hand whenever inline content changes. A build-step or scanning tool is essentially required to keep hashes in sync with deployed markup.
A nonce, end to end
The nonce is a random value generated on the server per response. It appears in the header and on every inline tag you want to allow:
# Response header (regenerated per request)
Content-Security-Policy:
script-src 'nonce-r4nd0mB4se64Value' 'strict-dynamic';
object-src 'none';
base-uri 'none';
# Matching inline script in the HTML body
<script nonce="r4nd0mB4se64Value">
// Inline application bootstrap goes here.
</script>Reference: content-security-policy.com — nonces
A hash, for static markup
When the inline content is fixed at build time, hash the script body (byte-for-byte) and add the encoded value to the directive:
Content-Security-Policy:
script-src 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc=';
object-src 'none';
base-uri 'none';
<script>console.log("hello world");</script>Need to compute one? Use the hash calculator — it builds CSP-ready sha256, sha384, and sha512 values for inline scripts and styles.
What 'strict-dynamic' actually does
When a trusted script (one that matches a nonce or hash) inserts a new <script> into the DOM, the new script inherits trust. This lets bundlers, feature-flag SDKs, and tag managers continue to work without maintaining sprawling host allowlists for every CDN their dependency tree might pull from.
Reference: web.dev — Mitigate XSS with a strict CSP