Skip to main content
Security November 13, 2026 · 8 min read

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

'self'

The page's own origin — same scheme, host and port. Not subdomains.

'none'

Nothing at all.

'unsafe-inline'

Allows inline <script> blocks, on* attributes and javascript: URLs. In script-src this disables most of the protection — it permits exactly what injection produces.

'unsafe-eval'

Allows eval() and new Function(). Some older libraries need it; avoid if you can.

'nonce-…'

Allows a script or style tag carrying the same random nonce attribute. The nonce must be unpredictable and new on every response.

'sha256-…'

Allows an inline script whose exact contents hash to this value. Good for static sites where a fresh nonce per response is not possible.

'strict-dynamic'

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

  1. Inventory what you load. Analytics, tag managers, fonts, payment widgets, chat, embeds — each needs its sources in the right directive.
  2. Ship it as Content-Security-Policy-Report-Only first. Nothing is blocked; the browser reports what would have been. Add a reporting endpoint with report-to (and report-uri for older browsers) and watch real traffic for a week or two.
  3. 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.
  4. 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