Content Security Policy (CSP) is a response header that tells the browser which scripts, styles, images, frames and connections a page may use. A browser that receives Content-Security-Policy: script-src 'nonce-abc123' 'strict-dynamic' will run only the <script> elements that carry nonce="abc123", plus whatever those scripts load, and will refuse an injected <script>alert(1)</script> even if an attacker manages to get it into the HTML. The current specification is the W3C Content Security Policy Level 3 Working Draft of 16 September 2026.
CSP limits what an injected payload can do; it does not stop the injection. Output encoding and template escaping still come first, and the policy is the layer that catches what gets past them.
Every browser result in this guide comes from a small Node test server and Chrome 152 on macOS, run on 2 October 2026. The policies and outputs quoted for ZeroTool’s CSP Header Generator are the generator’s actual output, and this site’s tests recompute them from the generator’s code.
How the browser applies a policy
A policy is a list of directives separated by semicolons. Each directive names a resource type and a source list:
Content-Security-Policy: default-src 'self'; img-src 'self' data:; object-src 'none'
When the browser needs to load or run something, it looks up the matching directive. If that directive is not in the policy, it walks a fallback list defined in CSP3 §6.8.3. A few of them:
| Request | Directives checked, in order |
|---|---|
<script> element (inline or src) | script-src-elem, script-src, default-src |
Inline event handler such as onclick | script-src-attr, script-src, default-src |
<style> element or stylesheet | style-src-elem, style-src, default-src |
| Worker | worker-src, child-src, script-src, default-src |
<iframe> | frame-src, child-src, default-src |
fetch(), XHR, WebSocket | connect-src, default-src |
Image, font, media, manifest, <object> | img-src, font-src, media-src, manifest-src, object-src, each falling back to default-src |
Chrome reports the fallback in its console. When a page had only script-src, the message for a blocked file read: “Note that ‘script-src-elem’ was not explicitly set, so ‘script-src’ is used as a fallback.”
base-uri, form-action and frame-ancestors are not fetch directives and have no fallback. A policy with only default-src 'self' still lets an injected <base href> rewrite relative script URLs and lets any site frame the page. Write those three explicitly.
Why allowlists fail and what makes a policy strict
The older style of policy lists the hosts a page may load scripts from, such as script-src 'self' https://cdn.example.com https://www.googleapis.com. Google’s Strict CSP guide calls these allowlist CSPs and says they “require a lot of customization and can be bypassed by attackers”. CSP3 §8.2 makes the same point about host-based policies on large origins such as CDNs: any script an allowed host serves, including ones the site never meant to use, passes the check.
A strict policy trusts individual script elements instead of hosts. The nonce-based version from web.dev is three directives:
Content-Security-Policy:
script-src 'nonce-{RANDOM}' 'strict-dynamic';
object-src 'none';
base-uri 'none';
'nonce-{RANDOM}'allows each<script>whosenonceattribute carries the same value. web.dev requires the value to be cryptographically random (“ideally 128+ bits in length”), new for every response, and base64-encoded.'strict-dynamic'lets a trusted script add more scripts withdocument.createElement('script'), so tag managers and widgets keep working without listing their hosts. CSP3 §8.2 describes the two effects: scripts requested by non-”parser-inserted” script elements are allowed, and host sources, scheme sources,'self'and'unsafe-inline'are ignored when loading script.object-src 'none'blocks plugins, andbase-uri 'none'blocks injected<base>tags.
A hash-based variant replaces the nonce with 'sha256-…' values for each inline script. It suits static pages, where no server can write a new nonce into every response.
What a nonce-based policy blocks in Chrome 152
The test server sent this header with a fresh 16-byte nonce on each request:
Content-Security-Policy: script-src 'nonce-<random>' 'strict-dynamic'; object-src 'none'; base-uri 'none'
The page tried seven things. A recorder script listening for securitypolicyviolation events wrote down what ran and what was blocked:
| What the page did | Result | Directive in the violation |
|---|---|---|
Inline <script nonce="…"> | Ran | — |
That script appended a <script src="/dyn.js"> with createElement | Ran | — |
That script called document.write('<script src="/dw.js">') | Blocked | script-src-elem |
That script called eval() | Threw EvalError | script-src |
Inline <script> without a nonce | Blocked | script-src-elem |
<script src="/self.js"> from the same origin, no nonce | Blocked | script-src-elem |
<button onclick="…">, clicked by the trusted script | Blocked | script-src-attr |
The document.write row is the parser-inserted rule in action: a script written into the parser is treated like markup, so 'strict-dynamic' does not cover it. Chrome’s messages for the last two rows:
Loading the script 'http://127.0.0.1:8851/self.js' violates the following Content Security Policy directive: "script-src 'nonce-…' 'strict-dynamic'". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked.
Executing inline event handler violates the following Content Security Policy directive 'script-src 'nonce-…' 'strict-dynamic''. … Note that hashes do not apply to event handlers, style attributes and javascript: navigations unless the 'unsafe-hashes' keyword is present. The action has been blocked.
Before enforcing a policy like this, move every onclick="" and javascript: URL into script, and replace document.write of scripts with createElement.
web.dev also gives a fallback for old browsers: script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline'. With that header, Chrome still blocked an inline script without a nonce and logged “Note that ‘unsafe-inline’ is ignored if either a hash or nonce value is present in the source list.” Browsers that understand nonces ignore the two fallback keywords, so they cost nothing.
A server that sends a new nonce on every response
The nonce has to be generated per response by whatever renders the HTML. This complete Node server does it with no dependencies:
// server.mjs: run with `node server.mjs`, then open http://localhost:8080/
import http from 'node:http';
import { randomBytes } from 'node:crypto';
const server = http.createServer((req, res) => {
if (req.url === '/widget.js') {
res.setHeader('Content-Type', 'text/javascript');
return res.end("document.body.append('widget loaded');");
}
const nonce = randomBytes(16).toString('base64'); // 128 bits, new for every response
res.setHeader('Content-Security-Policy',
`script-src 'nonce-${nonce}' 'strict-dynamic'; object-src 'none'; base-uri 'none'`);
res.setHeader('Content-Type', 'text/html; charset=utf-8');
res.end(`<!doctype html>
<script nonce="${nonce}">
const s = document.createElement('script');
s.src = '/widget.js'; // allowed: created by a trusted script
document.head.append(s);
</script>
<script>document.body.append('injected')</script>`);
});
server.listen(8080);
The second inline script has no nonce and is blocked, which is the behavior an injected script would get. Two details matter. The nonce must not be cached: a CDN that caches the HTML also caches the header and the nonce, and from then on every visitor gets the same value. And a nonce in a static file is a fixed string, not a nonce, which is why static sites use hashes.
Hashes for inline scripts that never change
A hash source is the base64 SHA-256, SHA-384 or SHA-512 of the exact text between <script> and </script>. For this inline script:
<script>document.documentElement.classList.add('js');</script>
the source is 'sha256-/x7W7R75k8Roq0WaVRQX9blP4OufE5xbAdzklGxsgpw='.
Any change to the text changes the hash, whitespace included. With one leading space, <script> document… hashes to 'sha256-tI4westzC+wr8k85EJ92dUyc/69jHuItpG4a/NvBSjw=' and the original hash no longer matches. In the test, Chrome blocked the edited copy and its console message included the hash it would have accepted, which is the fastest way to fix a mismatch caused by a build step that reformats inline code. Hashes cover inline <script> elements only; event handler attributes need 'unsafe-hashes', and 'unsafe-hashes' exists to let you migrate, not to keep.
Using the CSP Header Generator
The generator opens on its Strict preset in Enforce mode. Copied as an HTTP header without changes, it produces:
Content-Security-Policy: default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; upgrade-insecure-requests
It keeps the web.dev structure for script-src and adds directives for other resource types, so it is stricter than web.dev’s three-directive policy in some places and different in one. style-src 'self' blocks inline <style> and style="" attributes, which many CSS-in-JS libraries and analytics snippets use; switch to Moderate or add a hash if they break. base-uri 'self' allows a <base> pointing at your own origin, where web.dev uses 'none'.
'nonce-{RANDOM}' is a placeholder and the validation panel says so in a warning. Sending it unchanged is the most likely deployment mistake, so it was tested: with the Strict output as the response header, Chrome logged
The source list for the Content Security Policy directive 'script-src' contains an invalid source: ''nonce-{RANDOM}''. It will be ignored.
and then blocked every script on the page, including ones with a nonce attribute. The + nonce button on each directive card inserts the same placeholder; the tool never generates a nonce value itself, because a value copied from a web page would be the same on every response.
The four output tabs handle the placeholder differently:
- HTTP header and HTML
<meta>copy it as is. - Express (helmet) replaces it with a function that reads
res.locals.cspNonce, and adds a middleware that setsres.locals.cspNonce = crypto.randomBytes(16).toString('base64')for every request. Render that value into each<script nonce="…">. The code passesuseDefaults: false, so helmet does not merge its own defaults into your policy. - Nginx prefixes the
add_header … always;line with a comment explaining that Nginx cannot write the matching nonce into HTML your application renders, so the header belongs in the application.
The validation panel reacts to the policy as you edit it. Before 1 October 2026 the Strict preset was script-src 'self' 'strict-dynamic' with no nonce; if a saved policy still has that, the panel now warns that “every <script> on the page is blocked”, and Chrome agrees. Its message for that policy ended with “Note that ‘strict-dynamic’ is present, so host-based allowlisting is disabled.”
The hash calculator takes the contents of an inline script or style, computes the SHA-256, SHA-384 or SHA-512 with the Web Crypto API, and adds the source to script-src or style-src. It hashes the UTF-8 bytes of exactly what you paste, so paste what is between the tags, not the tags themselves.
The generator’s directive list still offers navigate-to and prefetch-src. Neither appears in the 16 September 2026 CSP3 draft, so do not rely on them.
<meta> delivery and its limits
A policy can also be delivered in the page: <meta http-equiv="Content-Security-Policy" content="…">. CSP3 §3.3 says the Report-Only header has no <meta> form and that “Neither are the report-uri, frame-ancestors, and sandbox directives” supported there. It also notes that a <meta> policy does not apply to content that comes before it, so the tag must be the first thing in <head>.
A test page with all three directives in a <meta> policy produced these console errors in Chrome, and the rest of the policy was enforced:
The Content Security Policy directive 'frame-ancestors' is ignored when delivered via a <meta> element.
The Content Security Policy directive 'report-uri' is ignored when delivered via a <meta> element.
The Content Security Policy directive 'sandbox' is ignored when delivered via a <meta> element.
The generator’s <meta> tab drops exactly those three directives and keeps report-to. With the Strict preset it outputs:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'nonce-{RANDOM}' 'strict-dynamic'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests">
Clickjacking protection therefore needs a real header, from a CDN, a reverse proxy or a Pages _headers file.
Report-Only first, then enforce
Content-Security-Policy-Report-Only evaluates the same policy without blocking anything. In the test, the page with a Report-Only strict policy ran both its unauthorized scripts; Chrome logged each one at info level with “The policy is report-only, so the violation has been logged but no further action has been taken”, and the securitypolicyviolation events had disposition: "report".
Reports reach you through report-uri or report-to:
report-uri /csp-reportdelivered one POST per violation within seconds, withContent-Type: application/csp-reportand a body containingdocument-uri,violated-directive,effective-directive,original-policy,disposition,blocked-uri(inlinefor inline scripts),line-numberandsource-file.report-tonames an endpoint declared in aReporting-Endpointsheader and uses the Reporting API. CSP3 deprecatesreport-uriin its favor and says that whenreport-tois present,report-uriis ignored. In this test the endpoint was plainhttp://127.0.0.1. A policy with onlyreport-to, and a policy with both directives, produced no report at all within 75 seconds of the visit, while thereport-uri-only policy delivered at once. Check that your own endpoint receivesreport-toreports, over HTTPS, before you dropreport-uri.
A rollout that follows web.dev and the OWASP CSP Cheat Sheet:
- Add nonces (or hashes) to every inline and external script your templates emit, and move inline event handlers into script.
- Send the policy as
Content-Security-Policy-Report-Onlywith a reporting endpoint. In the generator, switch Mode to Report-Only before copying; the page opens in Enforce. - Read the reports. Violations from your own pages need a nonce, a hash or a code change. Violations whose source is a browser extension can be filtered out.
- When reports from your own code stop, send the same policy as
Content-Security-Policy, and keep the reporting directive so new third-party code shows up.
Setting the header on common servers
The policy text is the same everywhere; what changes is how each server sends it.
# Nginx: only for static pages with hash sources; nonces must come from the application
add_header Content-Security-Policy "script-src 'sha256-/x7W7R75k8Roq0WaVRQX9blP4OufE5xbAdzklGxsgpw=' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
The Nginx documentation says add_header applies only to 200, 201, 204, 206, 301, 302, 303, 304, 307 and 308 responses unless always is given, and that add_header directives are inherited from the previous level “if and only if there are no add_header directives defined on the current level”. A location block with its own add_header for caching therefore drops the CSP set at the server level. Since Nginx 1.29.3, add_header_inherit merge changes that rule.
# Apache httpd: "always" also covers error responses and ErrorDocument redirects
Header always set Content-Security-Policy "script-src 'sha256-/x7W7R75k8Roq0WaVRQX9blP4OufE5xbAdzklGxsgpw=' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'"
mod_headers keeps two tables, onsuccess (the default) and always, and only headers in always are added “to the response even on error”.
Sources
- W3C Content Security Policy Level 3 (Working Draft, 16 September 2026): §3.3 the
<meta>element, §6.5 reporting directives, §6.8.3 fetch directive fallback lists, §8.2 usage of'strict-dynamic' - web.dev: Mitigate cross-site scripting with a strict Content Security Policy
- OWASP Content Security Policy Cheat Sheet
- Nginx ngx_http_headers_module, Apache mod_headers
Related tools: Hash Generator for SHA-256 outside the CSP context, .htaccess Generator for Apache security headers, and Meta Tag Generator for the rest of the <head>.