What a Content-Security-Policy does
A Content-Security-Policy (CSP) is a response header listing the sources a page may load scripts, styles, images, frames and other resources from. The browser blocks everything else. Its main job is to stop injected scripts from running when cross-site scripting gets past your output encoding.
CSP is a second layer. It limits the damage of an injection, while escaping untrusted data correctly remains the first defense.
The directives that matter
A policy is a list of directives separated by semicolons. Each directive names a type of resource or behavior and the sources allowed for it.
- default-src: the fallback for fetch directives you don't set, such as script-src, img-src and connect-src.
- script-src: where scripts may come from. This directive does most of the XSS protection.
- style-src: where stylesheets and inline styles may come from.
- img-src: image sources. Inline images often need data: as well.
- connect-src: the URLs scripts may contact through fetch, XMLHttpRequest, WebSocket and EventSource.
- object-src 'none': disables plugin content in object and embed elements, an old route for running code.
- base-uri 'none' or 'self': stops an injected base element from changing where relative script URLs point.
- frame-ancestors: which sites may frame the page. It doesn't fall back to default-src and is ignored in a meta tag.
- form-action: where forms may submit. It doesn't fall back to default-src either.
Nonces, hashes and strict-dynamic
Allowlisting script hosts is hard to get right, because many large domains host something an attacker can abuse, such as a JSONP endpoint or an old library version. Strict policies identify trusted scripts by nonce or hash instead.
A nonce is a random value of at least 128 bits, generated fresh for every response. It goes in the header as 'nonce-' followed by the value, and on each trusted script tag as a nonce attribute. An injected script without the right nonce is blocked, so never cache HTML with a fixed nonce.
A hash such as 'sha256-' plus the base64 digest allows one exact inline script. Hashes suit static pages that can't generate a nonce per response.
'strict-dynamic' passes trust from a nonced or hashed script to the scripts it loads, which keeps tag managers and bundle loaders working. Browsers that support it ignore host allowlists and 'self' in script-src.
A sample strict policy
This policy uses a nonce for scripts and keeps the other directives tight. It is split across lines for readability; send it as one header line, with a new nonce on every response.
style-src 'self' blocks inline styles. If your pages rely on them, add nonces or hashes for style elements, or accept 'unsafe-inline' in style-src, which is a smaller risk than in script-src.
Content-Security-Policy:
default-src 'self';
script-src 'nonce-9fKx2QeLm7RzT0pWb4Yc' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
connect-src 'self' https://api.example.com;
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
form-action 'self';
report-to csp
Reporting-Endpoints: csp="https://example.com/csp-reports"Roll out in report-only mode first
Start with the Content-Security-Policy-Report-Only header. The browser evaluates the policy and reports violations but blocks nothing, so you can find the inline scripts, third-party tags and endpoints the policy misses.
Reports go to the endpoint named by report-to and defined in a Reporting-Endpoints header. The older report-uri directive is deprecated, but browser support for report-to still varies, so many policies include both. A report-only policy cannot be set with a meta tag.
Switch to the enforcing header once the remaining reports are noise, such as browser extensions. You can send both headers at once, enforcing the current policy while a stricter one runs in report-only.
Common mistakes that weaken a policy
A policy can be present and still do little. Watch for these:
- 'unsafe-inline' in script-src with no nonce or hash, which allows any injected inline script to run. When a nonce or hash is present, current browsers ignore 'unsafe-inline', which is why strict policies include it only as a fallback for old browsers.
- 'unsafe-eval', which allows strings to run as code through eval and similar functions.
- Broad sources such as https:, * or an entire CDN domain in script-src. Anyone who can host a file there can run script on your page.
- Leaving out object-src 'none' and base-uri. object-src otherwise inherits default-src, and base-uri has no fallback at all.
- Expecting frame-ancestors, report-uri or report-only mode to work from a meta tag. They need HTTP headers.