A Content Security Policy (CSP) is a response header that declares to the browser 'which origins' resources this page may load.' Its main purpose is mitigating cross-site scripting (XSS) — stopping a malicious script an attacker injected from executing or exfiltrating data. You build a policy by listing directives, separated by semicolons, in the Content-Security-Policy header.
Building a policy: directives and default-src
A policy is made of per-resource-type directives: script-src (scripts), style-src (styles), img-src (images), connect-src (fetch/XHR/WebSocket), and so on. Any type you don't name falls back to default-src. So you design by locking down default-src tightly and opening specific types as exceptions.
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
frame-ancestors 'none';
upgrade-insecure-requests
nonce vs hash vs strict-dynamic
The hardest part is allowing inline scripts safely. 'unsafe-inline' permits every inline script and effectively defeats XSS protection, so avoid it. Use one of three tools instead.
- nonce: a random, single-use value the server generates per response and attaches to both the header (
script-src 'nonce-a1b2c3') and<script nonce="a1b2c3">. Only scripts whose value matches run, and an attacker can't predict the ever-changing nonce. - hash: allow an inline script by pre-computing the hash of its contents (
script-src 'sha256-...'). Handy when scripts are static and the server can't inject a dynamic nonce. - strict-dynamic: propagates trust from a nonce/hash-trusted script to the child scripts it loads dynamically. It doesn't rely on a host allowlist (
https://cdn...), so it resists allowlist-bypass attacks.
Content-Security-Policy:
script-src 'nonce-a1b2c3' 'strict-dynamic' https: 'unsafe-inline';
object-src 'none';
base-uri 'none'
In that example, a modern browser that supports strict-dynamic trusts only the nonce and ignores https: and 'unsafe-inline', while an older browser that doesn't understand it uses those as a fallback — giving you backward compatibility.
Rolling out with report-only — Enforcing CSP on an existing site right away tends to break the page by blocking legitimate resources too. So deploy first with the Content-Security-Policy-Report-Only header. This mode does not enforce the policy but reports violations only, letting you gather data on real user traffic about what would be blocked, refine the policy, and then flip to enforcement safely.
Reporting endpoints — To receive violation reports, specify a collection path. The old way used report-uri; the modern way defines a named endpoint via the Reporting-Endpoints header and points to it with report-to in the CSP. On a violation, the browser POSTs JSON — the blocked resource, the violated directive, and more — to that endpoint.
Reporting-Endpoints: csp-reports="https://example.com/csp-reports"
Content-Security-Policy-Report-Only:
default-src 'self'; report-to csp-reports
Common breakages and special directives
- Inline event handlers (
onclick="...") andjavascript:URLs are blocked by CSP. Move them toaddEventListenerin an external script. - Inline styles: an inline
styleattribute is blocked understyle-srcwithout a nonce/hash. To avoid'unsafe-inline', move styles into a file. frame-ancestors 'none': stops other sites from putting this page in an<iframe>, preventing clickjacking. It's the directive that supersedes the olderX-Frame-Options.upgrade-insecure-requests: automatically upgradeshttp://requests on the page tohttps://, reducing mixed-content problems.- Incremental hardening: don't try to write a perfect policy in one shot. The realistic order is observe with report-only, adjust from the reports, enforce, and then level up with nonce/strict-dynamic.