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 exactnonceattribute 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, andbase-uri 'none'stops an injected<base>tag from redirecting your relative script URLs. Usebase-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:
- Send the policy as
Content-Security-Policy-Report-Only. Browsers enforce nothing but report every violation. Collect reports with thereport-todirective and aReporting-Endpointsheader (many sites also add the olderreport-uridirective for wider browser support). - 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.