In one sentence
CSP is an HTTP response header that tells the browser which sources a page may load scripts, styles, images and frames from — so an injected script tag, even one that reaches the HTML, does not run.
Why it matters
Cross-site scripting has been on every top-ten list for twenty years because input validation always misses one field. CSP is the defence that works after the miss: the attacker's <script> lands in the page, and the browser refuses to execute it because the policy did not authorise it. The same header stops your page being framed for clickjacking (frame-ancestors), blocks data exfiltration to unknown hosts (connect-src), and — the reason PCI DSS v4 requirements 6.4.3 and 11.6.1 exist — keeps a payment page from loading a skimming script from a compromised third-party CDN.
It is also one of the few controls with a built-in feedback loop: report-to sends the browser's violation reports to you, so a deployment in report-only mode tells you what would break before anything does.
How a policy reads
Content-Security-Policy: default-src 'self';
script-src 'nonce-r4nd0m…' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
report-to csp-endpoint
- Directives name a resource type:
script-src,style-src,img-src,connect-src(XHR/fetch/WebSocket),frame-src,font-src,object-src.default-srcis the fallback for any not listed. - Sources:
'self'(same origin), a host or scheme,'none', and the keywords everyone misreads —'unsafe-inline'(allow inline scripts/styles — defeats the point for scripts) and'unsafe-eval'. - Nonces and hashes. A per-response random
'nonce-…'on the header and on each legitimate<script nonce="…">means only scripts the server put there run.'sha256-…'does the same for a fixed inline script. With'strict-dynamic', a nonced script may load further scripts, and host allow-lists are ignored — the modern "strict CSP" recipe. frame-ancestorsreplacesX-Frame-Options;base-uriandform-actionclose two injection side doors;upgrade-insecure-requestsfixes mixed content.Content-Security-Policy-Report-Onlyis the same header in dry-run mode: nothing blocked, everything reported.
This site runs a nonce-based policy on every page; the nonce rotates per request, which is why its pages are never cached as HTML.
Try it yourself hands-on
Your marketing site has one third-party analytics script, fonts from Google, and no CSP. A pentest report marks XSS as high because there is nothing to contain it.
- Paste the site's current response headers into the HTTP Security Headers Grader. Expect a low grade with "Content-Security-Policy: missing" at the top of the findings, plus whatever else is absent (HSTS,
X-Content-Type-Options). - Open the CSP Builder and start from the strict preset. Set
script-srcto a nonce with'strict-dynamic', addhttps://fonts.googleapis.comtostyle-srcandhttps://fonts.gstatic.comtofont-src,connect-src 'self' https://analytics.example,frame-ancestors 'none'. The builder assembles the header and flags'unsafe-inline'inscript-srcas the weakening it is. - Append a
report-to(or legacyreport-uri) directive pointing at a collector, and deploy the result asContent-Security-Policy-Report-Onlyfirst. After a week, the reports show what you forgot — typically an inlineonclickhandler and an image from a CDN. Fix the handler (move it to a nonced script), add the CDN toimg-src. - Switch the header to enforcing. Re-grade with the Headers Grader: CSP now present, grade up. For the analytics script itself, generate a Subresource Integrity hash with the SRI Generator so a changed file on the vendor's CDN is refused even though its host is allowed.
Common misreadings
- A host allow-list is weaker than it looks. Allowing
https://cdn.exampleallows every file on it, including the JSONP endpoint an attacker can abuse. Nonces plus'strict-dynamic'are why "strict CSP" exists. 'unsafe-inline'inscript-srcswitches CSP off for XSS purposes. It is acceptable instyle-srcas a transition, not inscript-src.- Report-only forever is not a control. Sites launch in report-only "for a week" and stay there for years. Put the enforcement date in the ticket.