All guides · Security

Content Security Policy (CSP)

Content Security Policy (CSP) is a response header that tells the browser which scripts, styles, frames and other resources a page is allowed to load and run. Anything the policy does not allow is blocked, and the browser can report the violation to you.

Why it matters

Cross-site scripting (XSS) happens when an attacker gets their own JavaScript to run on your page, for example through a comment field, a URL parameter or a compromised dependency. That script runs with the same rights as your own code: it can read what the visitor sees, act as the logged-in user, and send data elsewhere.

Escaping output correctly is the first line of defense. CSP is the second: if an injection slips through, a well-built policy stops the injected script from running. A policy only helps if it is strict about scripts. A CSP that allows inline scripts or scripts from any host looks reassuring but blocks very little.

What we check

We read the Content-Security-Policy header and any <meta http-equiv="Content-Security-Policy"> tag. If there is no enforced policy, we evaluate Content-Security-Policy-Report-Only instead. Script rules come from script-src, or from default-src when script-src is not set.

Check Passes when
Content Security Policy an enforced policy restricts scripts through script-src or default-src
CSP avoids ‘unsafe-inline’ scripts script sources do not include 'unsafe-inline', or also include a nonce or hash
CSP avoids ‘unsafe-eval’ script sources do not include 'unsafe-eval'
CSP has no wildcard script sources script sources do not include *, http:, https: or data:, unless 'strict-dynamic' is set
CSP sets object-src and base-uri object-src is set (or default-src 'none'), and base-uri is set

A Report-Only policy, or an enforced policy with neither script-src nor default-src, is reported as “needs attention”. The nonce and 'strict-dynamic' exceptions mirror browser behavior: when a nonce or hash is present, browsers ignore 'unsafe-inline', and when 'strict-dynamic' is present they ignore host and scheme sources such as https:.

How to fix

The recommended approach is a strict CSP based on nonces. Instead of listing every host you load scripts from, you mark the scripts you trust.

Content-Security-Policy:
  script-src 'nonce-R4nd0mV4lu3' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';

(Shown on several lines for readability; send it as a single header line.)

  • 'nonce-…': only <script> elements carrying this exact nonce attribute may run. Generate a new, unguessable value (at least 128 bits, base64-encoded) for every response, and add it to every script tag you output: <script nonce="R4nd0mV4lu3" src="/app.js"></script>.
  • 'strict-dynamic': scripts loaded by a trusted script are trusted too, so tag managers and bundlers that inject further scripts keep working.
  • https: and 'unsafe-inline' are fallbacks for very old browsers that do not understand nonces. Modern browsers ignore both when a nonce and 'strict-dynamic' are present, and our checks account for that.
  • object-src 'none' blocks plugin content, and base-uri 'none' stops an injected <base> tag from redirecting your relative script URLs. Use base-uri 'self' if your pages use a <base> tag.

If your pages are static and cached, a per-response nonce is not practical. Use hashes instead: 'sha256-…' of each inline script’s exact content, plus 'strict-dynamic'.

Most frameworks have built-in support for nonces or hashes, and a CDN or edge function can insert nonces for static sites. Generating the policy in one place, rather than in every template, keeps it consistent.

Inline event handlers and eval

Nonces do not cover inline event handlers such as onclick="…" or javascript: URLs. Move them into script files and attach handlers with addEventListener. Likewise, remove code that relies on eval(), new Function() or string arguments to setTimeout, so you can drop 'unsafe-eval'. Some older libraries and template engines need eval; upgrading them is usually the fix.

Roll it out safely

A wrong CSP breaks pages, so deploy it in two steps:

  1. Send the policy as Content-Security-Policy-Report-Only. Browsers enforce nothing but report every violation. Collect reports with the report-to directive and a Reporting-Endpoints header (many sites also add the older report-uri directive for wider browser support).
  2. Fix what the reports show: missing nonces, inline handlers, third-party scripts. Then switch the header name to Content-Security-Policy.

Keep a Report-Only header alongside the enforced one when you test future changes.

Nginx (static policy; nonces need application or edge support):

add_header Content-Security-Policy "script-src 'sha256-…' 'strict-dynamic'; object-src 'none'; base-uri 'none'" always;

Apache

Header always set Content-Security-Policy "script-src 'sha256-…' 'strict-dynamic'; object-src 'none'; base-uri 'none'"

Prefer the header over the <meta> tag: the meta tag cannot set frame-ancestors or reporting, and it only applies from the point it appears in the page.

Further reading: web.dev: Mitigate XSS with a strict CSP and MDN: Content-Security-Policy.

Last updated October 24, 2026