Content Security Policy: A Strict Nonce-Based Policy and How to Roll It Out Safely

A Content Security Policy limits what scripts a browser will run, which can turn a cross-site scripting bug from a full compromise into a blocked request. Here is the strict nonce-based policy MDN and OWASP recommend, what each directive does, and a report-only rollout that does not break your site.

Black-and-white photo of an open padlock hanging from the latch of a metal grille gate
Photo by Hennie Stander on Unsplash

A Content Security Policy (CSP) is an HTTP response header that tells the browser which sources of script and other content it may use on your page. The version worth deploying for cross-site scripting (XSS) defense is a strict policy based on a random per-response value called a nonce. MDN’s guidance reads: “To control script loading as a mitigation against XSS, recommended practice is to use nonce- or hash-based fetch directives.” OWASP’s cheat sheet likewise calls a strict CSP the “current leading practice.” This article covers the minimal strict policy, what each part does, and how to roll it out without a day of broken pages.

It is research-based. We tested only one thing ourselves: a small Node.js script confirming that the example below produces a different nonce on every response and that the header and page agree. We did not test browser behavior.

The minimal strict policy

MDN’s nonce-based example is:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}';
  object-src 'none';
  base-uri 'none';

In plain terms:

  • script-src 'nonce-{RANDOM}' allows a script only if its tag carries a nonce attribute matching the value in the header. Without nonce or hash expressions, MDN says, a script-src or default-src directive blocks inline JavaScript. That is the protection: an attacker who injects <script>alert(1)</script> does not know the nonce, so the browser refuses to run it.
  • object-src 'none' disallows plugins and embedded objects. OWASP’s strict samples set it because, with plugin-types removed from the specification, object-src 'none' is the control when you do not need embedded objects.
  • base-uri 'none' stops injected <base> elements from changing how relative URLs resolve. OWASP’s samples also set base-uri 'none'.

Add frame-ancestors 'none' if your pages should never be embedded in an iframe on another site. MDN says: “Unless you need your site to be embeddable, you should set frame-ancestors to 'none',” and describes it as a more flexible replacement for X-Frame-Options.

Generating the nonce

The rule from MDN: the nonce “must be different for every HTTP response, and must not be predictable.” Here is a minimal Node.js example (no framework) that does that:

const http = require("node:http");
const crypto = require("node:crypto");

http.createServer((req, res) => {
  const nonce = crypto.randomBytes(16).toString("base64"); // new value per response
  const policy = [
    `script-src 'nonce-${nonce}' 'strict-dynamic'`,
    "object-src 'none'",
    "base-uri 'none'",
    "frame-ancestors 'none'",
  ].join("; ");
  res.setHeader("Content-Security-Policy-Report-Only", policy); // switch to Content-Security-Policy to enforce
  res.setHeader("Content-Type", "text/html; charset=utf-8");
  res.end(`<!doctype html><script nonce="${nonce}">/* app code */</script>`);
}).listen(8080);

We ran an equivalent script twice against itself: the nonce differed between the two responses and the nonce in the body matched the header. That confirms the mechanics, not that your application is protected.

OWASP warns against one shortcut: do not use middleware that automatically adds the nonce to every script tag, because scripts an attacker injects would then receive the nonce too. The nonce belongs only on scripts your templates deliberately emit, which means your templating layer has to apply it.

Nonce, hash or 'strict-dynamic'

OptionGood forLimitation
NoncePages rendered by a server on each requestNeeds a templating step; a static, cached page would reuse the nonce, which is unsafe
Hash ('sha256-...')Static pages with fixed inline scriptsOWASP: any change to the script, even whitespace, changes the hash and breaks it; external scripts also need an integrity attribute (MDN)
'strict-dynamic'Scripts that load other scripts, such as tag managersMDN: it “does make your CSP less secure” because trusted scripts that create <script> elements from XSS-prone input are not protected

Per MDN, 'strict-dynamic' means that if a script has a nonce or hash, it may load further scripts that do not have one. It eases third-party scripts but moves some trust to them.

Why not just list allowed hostnames?

Allowlist policies such as script-src https://cdn.example.com were the original mechanism. OWASP says a strict policy is “much easier to deploy” and warns that a non-strict policy “that is too granular or permissive is likely to lead to bypasses.” We did not survey bypass techniques here and do not quantify how often they occur.

Roll it out in report-only mode first

The Content-Security-Policy-Report-Only header applies the policy in reporting mode. MDN: “The policy is not enforced, but any violations are sent to the reporting endpoint specified in the policy.” OWASP calls it non-blocking (“fail open”) and often a precursor to blocking mode. Practical points:

  1. Report-only cannot be set in a <meta> element, per MDN and the W3C draft. The <meta http-equiv> form also “does not support all CSP features.” Use response headers, on all responses, not only the main document (MDN).
  2. Both headers can run together. MDN says that if both are present, both policies are honored: the enforced one blocks and the report-only one reports. OWASP suggests running a strict report-only policy alongside a looser enforced one to collect many violation reports while you migrate.
  3. Collect reports. MDN’s recommended method uses the Reporting API:
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy: default-src 'self'; report-to csp-endpoint

Reports arrive as POST requests with content type application/reports+json and "type": "csp-violation". The older report-uri directive is described by MDN as deprecated, but it says to declare both until report-to is supported in all browsers. 4. Fix the causes, which are usually inline <script> blocks, inline event handlers such as onclick, and third-party snippets. Move inline code to external files or give emitted scripts a nonce. 5. Switch the header name from Content-Security-Policy-Report-Only to Content-Security-Policy once reports are quiet for your traffic, and keep reporting on.

Avoid the temptation to add 'unsafe-inline' to make reports go away. MDN: “Developers should avoid 'unsafe-inline', because it defeats much of the purpose of having a CSP.” It adds that when a directive has nonce or hash expressions, browsers ignore 'unsafe-inline' in it, so adding it beside a nonce does not weaken the policy in browsers that support nonces, but it also gains nothing there.

What CSP does not do

OWASP states that CSP “should not be relied upon as the only defensive mechanism against XSS.” MDN says the same thing differently: “Setting a CSP is not an alternative to sanitizing input. Websites should sanitize input and set a CSP.” Output encoding and safe templating remain the primary fix. CSP also does not address cross-site request forgery, misconfigured cross-origin access, or broken authorization; see our articles on CSRF protection, CORS misconfiguration and broken access control. For where missing security headers sit among other common API mistakes, see the most common API security misconfigurations.

A note on standards status: as of our check, CSP Level 3 is a W3C Working Draft (the page shows September 16, 2026 as its date), not a finished Recommendation. Browser support is what determines real behavior, so test the browsers your users have.

Sources and verification

Checked on October 10, 2026. We did not test a policy in any browser.

Important claimSourceVerification
Strict CSP is recommended practice for XSS mitigation; nonce must be unique per response and unpredictable; hashes for static pages; example policy; 'strict-dynamic' behavior and its security cost; 'unsafe-inline' warning and nonce/hash behaviorMDN, Content Security Policy (CSP)Verified
Report-Only header behavior, both headers honored, no meta support, Reporting-Endpoints and report-to, deprecated report-uriSame MDN pageVerified
frame-ancestors 'none' recommendation and relation to X-Frame-OptionsSame MDN pageVerified
Strict CSP is leading practice; nonce guidance and middleware warning; hash fragility; object-src and base-uri; Report-Only is fail-open; CSP not the only XSS defense; allowlist bypass warningOWASP, Content Security Policy Cheat SheetVerified
Report-only cannot be in meta; CSP Level 3 is a W3C Working Draft dated September 16, 2026W3C, Content Security Policy Level 3Qualified: draft status and date as shown when we checked; only the first part of the document was read
Example script produces a different nonce per response and matches the pageOur own test, Node.js 22Qualified: tests header and nonce mechanics only, not browser enforcement

Frequently asked questions

What does a Content Security Policy protect against?

MDN describes CSP as a set of instructions from a site to the browser that restricts what the site's code is allowed to do. Its main use is defense in depth against cross-site scripting (XSS): even if an attacker manages to inject markup, a strict policy can stop the injected script from running. It does not remove the underlying injection bug.

Should I use a nonce or a hash?

MDN says nonces suit pages your server renders per request, and hashes suit static pages. A nonce is a random value generated for each response and placed in both the header and the script tags. A hash allows one specific script and breaks if the script changes at all, including whitespace, per OWASP.

Is 'unsafe-inline' ever acceptable?

MDN advises avoiding it because it defeats much of the purpose of a CSP, since inline JavaScript is one of the most common XSS vectors. MDN also notes that when a directive contains nonce or hash expressions, browsers ignore 'unsafe-inline' in that directive.

Can I test a policy without breaking my site?

Yes. Send the policy in the Content-Security-Policy-Report-Only header. Violations are reported to your endpoint but nothing is blocked. If both headers are present, both are honored: the enforced one blocks and the report-only one generates reports.

Does CSP replace X-Frame-Options?

MDN describes the frame-ancestors directive as a more flexible replacement for X-Frame-Options and recommends setting it to 'none' unless your site needs to be embeddable. We did not test every browser version; check your support requirements.

AppSecAPI Security