What Is Content Security Policy? A Practical Guide
Content Security Policy is a response header that tells the browser which scripts, styles, images and connections a page is allowed to use. Done well, it turns a cross-site scripting bug from "attacker runs code in your users' sessions" into "attacker's script is blocked and you get a report". Done carelessly, it either breaks your checkout or protects nothing.
The problem CSP solves
Cross-site scripting (XSS) happens when text an attacker controls ends up in your page as code — a comment, a search term or a profile name that slips past escaping and arrives as <script> or onerror=. The browser cannot tell your script from theirs; both come from your domain.
CSP gives the browser a second opinion. The server declares where legitimate code comes from, and anything else is refused, even if it made it into the HTML. It is a safety net under correct escaping, not a replacement for it.
Anatomy of a policy
A policy is a list of directives, separated by semicolons, each followed by the sources it allows:
Content-Security-Policy: default-src 'self'; img-src 'self' data: https://images.example-cdn.com; script-src 'self' https://analytics.example.com; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
The header goes on one line in practice; it is wrapped here for reading. default-src is the fallback for any fetch directive you don't list.
| Directive | Controls |
|---|---|
| script-src | JavaScript: files, inline blocks, event handlers, eval. The one that matters most. |
| style-src | Stylesheets and inline styles. |
| img-src, font-src, media-src | Images, web fonts, audio and video. |
| connect-src | fetch, XHR, WebSocket and beacon destinations — where data can be sent. |
| frame-src | What your page may embed in iframes. |
| frame-ancestors | Who may embed your page. The modern replacement for X-Frame-Options; protects against clickjacking. |
| object-src | Plugins (<object>, <embed>). Set to 'none'. |
| base-uri | Where a <base> tag may point. Restrict it, or injected <base> tags can redirect relative script URLs. |
| form-action | Where forms may submit. |
| upgrade-insecure-requests | Rewrites http:// sub-resources to https://. |
Source keywords
The page's own origin — same scheme, host and port. Not subdomains.
Nothing at all.
Allows inline <script> blocks, on* attributes and javascript: URLs. In script-src this disables most of the protection — it permits exactly what injection produces.
Allows eval() and new Function(). Some older libraries need it; avoid if you can.
Allows a script or style tag carrying the same random nonce attribute. The nonce must be unpredictable and new on every response.
Allows an inline script whose exact contents hash to this value. Good for static sites where a fresh nonce per response is not possible.
Trust propagates: a script allowed by nonce or hash may load further scripts, and host allowlists are ignored.
Allowlists versus nonces
The policy above is an allowlist: it trusts every script served from listed hosts. That looks tidy and is weaker than it seems. If any allowed host serves something an attacker can steer — a JSONP endpoint, an old copy of a framework, a user-upload bucket on a shared CDN — the attacker loads it from there and the policy waves it through. Research by Google in 2016 found the large majority of real-world allowlist policies could be bypassed this way.
A strict CSP trusts tags instead of hosts:
Content-Security-Policy: script-src 'nonce-R4nd0mV4lu3' 'strict-dynamic'; object-src 'none'; base-uri 'none' <script nonce="R4nd0mV4lu3" src="/app.js"></script>
Only script tags carrying this response's nonce run; scripts they load are trusted in turn; an injected tag has no nonce and is blocked. The server generates a new random nonce for every page view. Where that is impossible, list 'sha256-…' hashes of your inline scripts instead. The hash is the base64 SHA-256 of the exact text between the tags — not the hex digest most hash tools print:
printf '%s' 'console.log("hi")' | openssl dgst -sha256 -binary | openssl base64
# → paste as 'sha256-<that value>' in script-src Rolling it out without breaking things
- Inventory what you load. Analytics, tag managers, fonts, payment widgets, chat, embeds — each needs its sources in the right directive.
- Ship it as
Content-Security-Policy-Report-Onlyfirst. Nothing is blocked; the browser reports what would have been. Add a reporting endpoint withreport-to(andreport-urifor older browsers) and watch real traffic for a week or two. - Fix the sources, not the policy. Move inline
onclick=handlers into scripts; give legitimate inline scripts a nonce. Resist adding'unsafe-inline'to make reports go away. - Enforce, keep the reports flowing, and re-check whenever you add a third-party service. Tag managers in particular inject new scripts long after launch.
Header or meta tag?
Use the HTTP header whenever you can. A <meta http-equiv="Content-Security-Policy"> tag works for most directives, but it cannot set frame-ancestors, sandbox or reporting, cannot be report-only, and does not cover anything the page loads before the tag is parsed. Static hosts such as Cloudflare Pages, Netlify and Vercel all let you set headers through a config file, so the meta tag is rarely the only option.
Build a starting policy, then test it report-only