What CSP is
A Content Security Policy is a response header (or <meta http-equiv>)
that lists the origins, hashes, or nonces from which the browser
will load each kind of resource. The browser blocks any load that
does not match the policy.
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-rAnD0m';
style-src 'self' 'unsafe-inline';
img-src 'self' https: data:;
connect-src 'self' https://analytics.example;
frame-ancestors 'self';
base-uri 'none';
form-action 'self';
report-to csp-endpoint;
The policy applies per page load. Every directive defaults to
default-src if unset, except frame-ancestors and form-action,
which have their own defaults.
Why CSP matters
CSP closes the exploit cliff of cross-site scripting (XSS). A
classic XSS payload — <script>fetch('/api/admin/...')</script>
injected into a comment field — does nothing under
script-src 'self' 'nonce-…' because the injected script lacks
the page’s nonce. About 60% of OWASP-Top-10 XSS findings in the
OWASP 2021 statistics would be defanged by
a strict CSP.
CSP also defends against:
- Click-jacking via
frame-ancestors. - Form-hijacking via
form-action. <base>tag injection viabase-uri.- Unintended subresource loads via
default-srcplus per-type directives.
The three modes of trust
Inside script-src, the three trust mechanisms are:
- Origin —
'self',https://cdn.example. Permits all scripts from that origin. Coarse; the easiest to author and the easiest to abuse if any script on that origin is itself compromised. - Nonce —
'nonce-<base64>'. The server emits a fresh random nonce per response, embeds it in the header and in each<script nonce="...">tag. Per-response, per-script. - Hash —
'sha256-<base64>','sha384-...'. The hash of the script body. Suitable for static inline scripts whose content does not change.
Modern recommendations from the Mozilla CSP guide
favour nonces over origin allowlists for script-src because
allowlists tend to drift wider over time; in Lukas Weichselbaum’s 2018 CSP study,
about 95% of allowlist-based CSPs were bypassable via a single
trusted-origin script.
strict-dynamic
'strict-dynamic' says: trust whatever a nonce-validated script
loads. Once you trust the entry-point script via nonce, transitive
loads it makes are also trusted, regardless of origin. This makes
script bundlers and dynamic-import patterns work without listing
every chunk URL:
Content-Security-Policy:
script-src 'nonce-rAnD0m' 'strict-dynamic';
object-src 'none';
base-uri 'none';
The combination is the recommended modern policy. Cross-engine support reached interop in 2018.
Reporting
Content-Security-Policy-Report-Only runs the policy in audit
mode: violations are reported but not blocked. Useful for
gradual rollout.
report-to <group> plus a Reporting-Endpoints: <group>="<url>"
header collects violation reports as JSON. Per the Reporting API spec,
each report includes the directive that fired, the blocked URI,
the source file and line of the inline violation, and a sample of
the matched element. About 0.1% of users encounter a CSP-blocked
resource on a typical site, but the reports surface 100% of
production XSS attempts.
Common pitfalls
'unsafe-inline'inscript-src. Defeats almost the entire point of CSP. Use nonces instead.'unsafe-eval'inscript-src. Permitseval,new Function, andsetTimeout(stringArg). Most modern bundlers no longer require it.- Forgetting
object-src 'none'.<object>and<embed>bypassscript-srcin older engine versions. Lock them down. - Forgetting
base-uri. A<base href="...">injection redirects every relative URL to the attacker’s origin. - Wildcard schemes.
script-src https:allows any HTTPS host. Equivalent to no CSP for script. - Inline event handlers.
<button onclick="…">is treated as inline script and blocked. Bind handlers via JS instead. <style>tag with no nonce. Locks downstyle-srcsimilarly. Either nonce inline<style>or move to external stylesheets.- Mixed-content loads on HTTPS pages. Browsers upgrade or
block, depending on CSP.
upgrade-insecure-requestsdirective rewriteshttp:references tohttps:.
Cross-engine support
CSP Level 1 (origin-based) reached interop in 2014. CSP Level 2
(nonces, hashes) reached interop in 2016. CSP Level 3
('strict-dynamic', report-to, Trusted Types interop) reached
roughly 95% interop by 2024 across Chromium (Blink), WebKit, and
Gecko.
caniuse for CSP3
reports global support of about 97% for L2 features. L3 is at
about 88% — Trusted Types and report-to lag in WebKit.
Further reading
- W3C Content Security Policy Level 3.
- csp-evaluator.withgoogle.com
— Google’s policy linter; flags
'unsafe-inline', unsafe schemes, missingobject-src, and other common holes. - The Trusted Types specification
pairs with CSP via
require-trusted-types-for 'script'and closes the remaining DOM-XSS sinks. - Mozilla’s Web Application Security Guidelines is the most usable per-directive reference.
- The OWASP CSP Cheat Sheet has worked migrations from common starting points.