Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Web Security (OWASP Top 10:2025)  Module 6 – A05:2025 Injection

Content Security Policy that actually holds

Build a strict nonce or hash Content Security Policy with strict-dynamic, deploy it in report-only mode, and avoid the patterns that defeat a policy.

  • Advanced
  • 25 minutes
  • Examples run with Node.js 24.21.0, quickjs 0.32.0, Python 3.14.8, Pyodide 314.0.7 and sql.js 1.14.2
  • By MySmartCoPilot
Free intro, Pro: 6 of 25 minutes free
Free: A policy holds when only scripts that carry this response's mark can run. With a pass: the other 7 sections and 3 interview answers, about 19 minutes.

What you will learn

  • Explain what a Content Security Policy mitigates and what it does not
  • Build a strict nonce-based or hash-based policy with strict-dynamic, object-src none and base-uri none
  • Deploy a policy in report-only mode, collect the reports and tighten it safely
  • Avoid the patterns that weaken a policy, such as unsafe-inline, unsafe-eval, wildcard sources and host lists
  • Generate a new nonce for every response on a dynamic server, and hashes for a static site

Before you start

What you’ll learn · Before you start

What you will learn

  • Explain what a Content Security Policy mitigates and what it does not
  • Build a strict nonce-based or hash-based policy with strict-dynamic, object-src none and base-uri none
  • Deploy a policy in report-only mode, collect the reports and tighten it safely
  • Avoid the patterns that weaken a policy, such as unsafe-inline, unsafe-eval, wildcard sources and host lists
  • Generate a new nonce for every response on a dynamic server, and hashes for a static site

Before you start

On this page

A Google study of 1,680,867 hosts that sent a Content Security Policy found that 99.34% of them used a policy that gave no real protection against cross-site scripting, and 75.81% of the unique policies relied on script allowlists that can be bypassed. A Content Security Policy: A response header that tells the browser which scripts, frames, forms and other resources a page may use, so that an injected script can be refused. It limits the damage of a flaw and does not remove the flaw. is a response header that tells the browser which scripts, frames, forms and other resources a page may use, and a policy that holds is one where only the scripts the server marked can run.

You will compare a host list with a nonce on the same scripts, build the strict policy one directive at a time, give a dynamic page a nonce and a static page a hash, deploy a policy in report-only mode and sort its reports, and learn the patterns that quietly defeat a policy.

A policy holds when only scripts that carry this response’s mark can run

A policy is defence in depth. It limits what an injected script can do when output encoding fails, and it does nothing about the flaw itself. MDN lists what it covers: scripts, through script-src and its relatives; clickjacking, through frame-ancestors; plain-HTTP subresources, through upgrade-insecure-requests; and DOM sinks, through Trusted Types. It does not stop server-side flaws such as SQL injection, and OWASP’s cheat sheet says it should not be the only defence against cross-site scripting. Three sources make a script runnable. A nonce: A random value, new for every response, that the server writes in the Content-Security-Policy header and in the nonce attribute of each script it trusts. The browser runs only the script tags whose nonce matches. is a random value, new for every response, that the server writes in the header and on each script tag it trusts. A hash source: A script-src entry such as 'sha256-' followed by a base64 value, which names a script by the SHA-256 hash of its exact text. Any change to the text, even a space, gives another hash and blocks the script. names a script by the SHA-256 hash of its exact text. And 'strict-dynamic' lets a trusted script add further scripts while the browser ignores host lists. The program below judges seven scripts against four policies, a host list first, and marks every wrong answer with a star:

Seven scripts judged by four policies JavaScript · verdicts/csp_verdicts.mjs
// A simplified model of how a browser judges one script against the script-src of a policy. It follows the rules that
// matter here: a nonce allows a script; 'unsafe-inline' is ignored when the list holds a nonce; 'strict-dynamic' ignores
// host sources and trusts scripts that a trusted script adds; 'unsafe-eval' lets strings become code. Hash sources are
// left out: the program on nonces and hashes, further down, computes them.
const PAGE = 'https://app.example/';

function allows(policy, script) {
  const directives = Object.fromEntries(policy.split(';').map((d) => d.trim().split(/\s+/)).filter((d) => d[0]).map(([name, ...values]) => [name, values]));
  const list = directives['script-src'] ?? directives['default-src'];
  if (!list) return true;
  const nonces = list.filter((t) => t.startsWith("'nonce-")).map((t) => t.slice(7, -1));
  const strict = list.includes("'strict-dynamic'");
  const unsafeInline = list.includes("'unsafe-inline'") && nonces.length === 0;
  const hostAllowed = (url) => list.some((t) => (t === "'self'" ? url.startsWith(PAGE) : url.startsWith(t)));
  const nonceMatches = !!script.nonce && nonces.includes(script.nonce);
  if (script.kind === 'inline') return nonceMatches || unsafeInline;
  if (script.kind === 'handler') return unsafeInline;
  if (script.kind === 'eval') return list.includes("'unsafe-eval'");
  if (nonceMatches) return true;
  if (strict) return !!script.addedByTrustedScript;
  return hostAllowed(script.url);
}

const policies = [
  ['allowlist', "script-src 'self' https://cdn.example"],
  ['+ unsafe-inline', "script-src 'self' 'unsafe-inline' https://cdn.example"],
  ['nonce, no dynamic', "script-src 'nonce-R4nd0m'; object-src 'none'; base-uri 'none'"],
  ['nonce + dynamic', "script-src 'nonce-R4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'"],
];
// [what the script is, the script, whether the page wants it to run]
const scripts = [
  ['own inline script', { kind: 'inline', nonce: 'R4nd0m' }, true],
  ['own file, /app.js', { kind: 'external', url: 'https://app.example/app.js', nonce: 'R4nd0m' }, true],
  ['file added by our loader', { kind: 'external', url: 'https://widgets.example/w.js', addedByTrustedScript: true }, true],
  ['injected inline script', { kind: 'inline' }, false],
  ['injected tag, file on the CDN', { kind: 'external', url: 'https://cdn.example/uploads/any.js' }, false],
  ['inline onclick attribute', { kind: 'handler' }, false],
  ['eval of a string', { kind: 'eval' }, false],
];

console.log('* marks a wrong answer for this page: a script let in, or one the page needs blocked.');
const letIn = policies.map(() => 0);
const refused = policies.map(() => 0);
console.log(`${''.padEnd(30)}${policies.map(([name]) => name.padEnd(19)).join('')}`);
for (const [label, script, wanted] of scripts) {
  const cells = policies.map(([, policy], i) => {
    const ok = allows(policy, script);
    if (ok && !wanted) letIn[i]++;
    if (!ok && wanted) refused[i]++;
    return `${ok ? 'runs' : 'blocked'}${ok !== wanted ? ' *' : ''}`.padEnd(19);
  });
  console.log(`${label.padEnd(30)}${cells.join('')}`);
}
console.log(`${'lets in what it should not'.padEnd(30)}${letIn.map((n) => String(n).padEnd(19)).join('')}`);
console.log(`${'blocks what the page needs'.padEnd(30)}${refused.map((n) => String(n).padEnd(19)).join('')}`);

Output

* marks a wrong answer for this page: a script let in, or one the page needs blocked.
                              allowlist          + unsafe-inline    nonce, no dynamic  nonce + dynamic    
own inline script             blocked *          runs               runs               runs               
own file, /app.js             runs               runs               runs               runs               
file added by our loader      blocked *          blocked *          blocked *          runs               
injected inline script        blocked            runs *             blocked            blocked            
injected tag, file on the CDN runs *             runs *             blocked            blocked            
inline onclick attribute      blocked            runs *             blocked            blocked            
eval of a string              blocked            blocked            blocked            blocked            
lets in what it should not    1                  3                  0                  0                  
blocks what the page needs    2                  1                  1                  0                  

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node csp_verdicts.mjs

The host list let in the injected tag that points at a file on the allowed CDN, and it blocked two scripts that the page needs, its own inline script and the file its loader adds. Adding 'unsafe-inline' let in three scripts that nobody meant to run: the injected inline script, the file on the CDN and the onclick attribute. The nonce policy without 'strict-dynamic' let in none, and still blocked the loader’s file. With 'strict-dynamic' the last column has no star at all, because the page’s own tags carry the nonce and the file its loader adds is trusted by that loader. Report-Only mode: Sending a policy in the Content-Security-Policy-Report-Only header, so that violations are reported and nothing is blocked. It shows what an enforced policy would break before it is enforced. sends the same policy in a header that reports violations and blocks nothing, which is how a policy like that last column reaches a real site. Step through one response under that policy:

Interactive · message by message One response, one nonce, one injected tag Step through a single response and watch which script tags carry the nonce and which do not.
One response, one nonce, one injected tag, step 6 of 6. Messages so far: Your server to The browser: header + page; The browser to An injected tag: blocked; Your server to The browser: a new nonce.Your servermakes a new valueThe browserruns the matching tagsAn injected tagblockedheader + pageblockeda new nonce

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A nonce for this response. The server makes a new random value for this response alone and keeps it in a variable. No other response will carry it.
  2. Header and tags carry it. The server writes the nonce into the policy header and into the nonce attribute of each script tag that it wrote itself. It sends the header and the page together.
  3. A flaw adds a tag. The page also contains a script tag that nobody wrote on purpose, put there by an injection flaw. It has no nonce, because whoever used the flaw could not know a value that is made fresh for each response.
  4. The browser compares. For each script tag the browser compares its nonce attribute with the nonce in the policy. The server's own tags match, so they run.
  5. The injected tag is blocked. The injected tag has no nonce, so the browser refuses it, and a reporting directive sends a report. The flaw is still in the page, but its script does not run.
  6. The next response. The next response gets a new nonce. A value that someone learned from one page is worth nothing on the next, because the server never uses it again.

The flaw that added the tag is still in the page after the last step, and the tag does nothing. That is the whole promise of a policy that holds, and the reason it is a second layer, never a replacement for encoding the output.

Checkpoint

Check: What makes a policy hold

  1. Question 1 of 2 In the run with seven scripts and four policies, which policy let in no script the page does not want and blocked none that it needs?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The nonce with strict-dynamic

    A script runs under the strict policy because it carries this response's nonce or was added by a script that does. Nothing in the policy depends on where a script comes from.

  2. Question 2 of 2 A team has a strict policy and proposes to stop encoding output. What is wrong with that?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A policy limits an injected script but does not remove the flaw or fix server-side bugs

    Defence in depth means a second control stays useful when the first fails. A policy is that second control for cross-site scripting, and it cannot take the place of the first.

Flashcards · 4 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is a Content Security Policy, and what does a policy that holds mean?

    A response header that tells the browser which scripts, frames, forms and other resources a page may use; one that holds lets only the scripts the server marked run. Google's study of 1,680,867 hosts that sent a policy found that 99.34 % gave no real protection against XSS, and 75.81 % of the unique policies relied on script allowlists that can be bypassed.

  2. What does CSP cover, and what is it not?

    Scripts (script-src and relatives), clickjacking (frame-ancestors), plain-HTTP subresources (upgrade-insecure-requests) and DOM sinks (Trusted Types). It is defence in depth: when output encoding fails, it restricts what an injected script is able to do. It does nothing about the flaw itself or about server-side flaws such as SQL injection, and it should not be the only defence against XSS.

  3. What three sources make a script runnable under a strict CSP?

    A nonce: a random value generated afresh for each response, which appears both in the header and on every script tag the server trusts; a hash source, the SHA-256 hash of a script's exact text; and 'strict-dynamic', which lets a trusted script add further scripts while the browser ignores host lists.

  4. Why does a host-list policy fail, and why is 'unsafe-inline' no repair?

    In the lesson a host list let in an injected tag pointing at a file on the allowed CDN while blocking the page's own inline script and the file its loader adds. Adding 'unsafe-inline' let in three scripts nobody meant to run, including the injected inline script and an onclick attribute. A nonce policy with 'strict-dynamic' judged all seven scripts correctly, and Report-Only mode lets such a policy reach a real site by reporting violations without blocking.

Build the strict policy directive by directive, and audit it Pro: opens with a Pro, Premium or Ultimate pass

In this part:

  • Use nonces on dynamic pages and hashes on static ones
  • Roll out in report-only mode and triage the reports by cause
  • Patterns that quietly defeat a policy, and what replaces them
  • Practise: a policy for three pages
    • A Content Security Policy for three pages
  • Interview questions
    • What does a Content Security Policy do, and what does it not do?
    • Your app sends a policy with a CDN host, 'self' and 'unsafe-inline' in script-src. The security team wants a strict policy without an outage. Describe the migration and the first things that break.
    • A static marketing site on a CDN has four inline scripts and wants a policy without 'unsafe-inline'. Nonces are not possible. What do you do, and what will surprise the team?
    • A review finds a policy with a wildcard CDN host in script-src and no object-src or base-uri. The team says CSP is done because cross-site scripting reports fell to zero. What do you say, and what do you change?
  • Key takeaways

1 diagram · 3 runnable examples (JavaScript) · 1 exercise with sample tests · 3 interview answers · about 35 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Terms in this lesson

Content Security Policy
A response header that tells the browser which scripts, frames, forms and other resources a page may use, so that an injected script can be refused. It limits the damage of a flaw and does not remove the flaw.
hash source
A script-src entry such as 'sha256-' followed by a base64 value, which names a script by the SHA-256 hash of its exact text. Any change to the text, even a space, gives another hash and blocks the script.
nonce
A random value, new for every response, that the server writes in the Content-Security-Policy header and in the nonce attribute of each script it trusts. The browser runs only the script tags whose nonce matches.
Report-Only mode
Sending a policy in the Content-Security-Policy-Report-Only header, so that violations are reported and nothing is blocked. It shows what an enforced policy would break before it is enforced.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.