Back to the header reference
Content

Content-Security-Policy

CSP tells the browser which sources of script, style, images and frames are legitimate, so injected content is refused even when an injection succeeds.

Recommended

Send this header. There is no meaningful downside for a typical site.

What it does

Content-Security-Policy is a allowlist the browser enforces on your behalf. You declare where resources may come from, and anything else is blocked before it executes. Its main target is cross-site scripting: an attacker who manages to get `<script>alert(1)</script>` into your page still achieves nothing if the browser has been told inline script is not allowed to run.

That last clause is where most policies fail. A policy containing `unsafe-inline` in `script-src` permits exactly the thing XSS relies on, which means the header is present, reported as present by scanners, and providing no XSS protection whatsoever. The same is true of `unsafe-eval`, and of a `script-src` that allowlists a CDN hosting a library with a known gadget — allowlists have been demonstrated to be bypassable on a majority of real sites.

The approach that actually works is a nonce or a hash. Generate a random nonce per response, put it on every legitimate `<script>` tag, and set `script-src 'nonce-<value>' 'strict-dynamic'`. Injected script has no nonce and does not run. `strict-dynamic` lets your own trusted scripts load their dependencies without you enumerating every CDN, which is what makes the approach survive contact with a real application.

Deploy it with `Content-Security-Policy-Report-Only` first. That header enforces nothing and reports everything, so you can find what your own site breaks before your users do. Run it for a fortnight, read the reports, then switch the header name.

What to send

Content-Security-Policy: default-src 'self'; script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'

There is no universal CSP — the policy is a description of your application. But this shape is the one worth starting from: a nonce rather than an allowlist, `object-src 'none'` because Flash and friends are pure liability, `base-uri 'none'` because an injected `<base>` tag redirects every relative URL on the page, and `frame-ancestors` because it supersedes X-Frame-Options with finer control.

Directives and values

default-src

The fallback for every fetch directive you do not set explicitly. Setting it to `'self'` and then overriding specifics is the sane way to build a policy.

script-src

Where script may load from, and whether inline script may run. The directive that decides whether your CSP stops XSS or not.

'nonce-<base64>'

A per-response random token. Script tags carrying the matching `nonce` attribute run; injected ones do not. Must be unpredictable and regenerated on every response — a static nonce is worse than none, because it looks like protection.

'strict-dynamic'

Lets a script that was itself trusted (by nonce or hash) load further scripts. Makes nonce-based policies workable for apps with dynamic loaders, and causes host allowlists in the same directive to be ignored by supporting browsers.

frame-ancestors

Who may embed this page in a frame. The modern replacement for X-Frame-Options, and unlike it, supports multiple origins.

base-uri

Restricts what `<base href>` may be set to. Rarely set, and its absence lets an injected `<base>` tag silently repoint every relative link and script on the page.

object-src

Controls `<object>`, `<embed>` and `<applet>`. Should be `'none'` on essentially every modern site.

report-to / report-uri

Where violation reports are sent. `report-uri` is deprecated but still the more widely supported of the two, so sending both is common.

Common mistakes

  1. Including 'unsafe-inline' in script-src. This permits exactly the injection CSP exists to stop. A policy with it is not a weak CSP, it is a decorative one — any XSS that would have worked without the header still works. If inline script is unavoidable, use a nonce or a hash for those specific blocks.
  2. Trusting a host allowlist. Allowlisting a large CDN usually allowlists some JSONP endpoint or AngularJS build hosted on it, which is enough to execute arbitrary script. Nonce plus `strict-dynamic` avoids the whole class of bypass.
  3. Reusing the same nonce across responses. A nonce is only meaningful because an attacker cannot predict it. Generating one at build time, or caching a page with its nonce embedded, turns it into a fixed password that is written in the page source.
  4. Setting the policy in a <meta> tag and expecting parity. The `<meta http-equiv>` form works but cannot carry `frame-ancestors`, `report-uri` or `sandbox`, and it applies only from where it appears in the document onward. Send the real header where you can.
  5. Going straight to enforcement. A first CSP almost always breaks something — an analytics snippet, a chat widget, a stylesheet loaded from a marketing tool. `Content-Security-Policy-Report-Only` gives you the full violation stream with nothing blocked. Skipping that step is how a policy gets rolled back and never retried.

Browser support

CSP Level 2 is universal. `strict-dynamic` and nonces (Level 3) are supported by every current browser; older browsers that do not understand `strict-dynamic` fall back to the host allowlist in the same directive, which is why keeping a conservative allowlist alongside it is a reasonable belt-and-braces move. `report-to` has thinner support than `report-uri`, so most deployments send both.

Frequently asked questions

What is a good starting Content-Security-Policy?

Start in report-only mode with `default-src 'self'; object-src 'none'; base-uri 'none'` and add what your own violation reports tell you is genuinely needed. Then move script to a per-response nonce with `'strict-dynamic'` before switching the header from report-only to enforcing.

Why is 'unsafe-inline' bad in a CSP?

Because inline script execution is the mechanism nearly all cross-site scripting depends on. A policy containing `unsafe-inline` in `script-src` allows injected inline script to run exactly as it would with no CSP at all, so the header provides no XSS protection while appearing to.

What is the difference between CSP nonces and hashes?

A nonce is a random token generated per response and placed on trusted script tags; a hash is the base64 SHA of a specific inline script block. Nonces suit dynamic pages, hashes suit static content that can be hashed at build time. Both allow specific inline script without opening the door to all of it.

Does Content-Security-Policy replace X-Frame-Options?

The `frame-ancestors` directive does, and takes precedence over X-Frame-Options where both are present. It is also more capable, since it accepts multiple origins. Keeping X-Frame-Options alongside it costs nothing and covers very old clients.

How do I find what my CSP is breaking?

Deploy it as `Content-Security-Policy-Report-Only` with a `report-uri` or `report-to` endpoint. Nothing is blocked, every violation is reported, and the browser console lists them during development. Leave it running long enough to cover infrequent pages — a fortnight is a reasonable minimum.

Check this header on your own site

The free Security Headers Checker reads Content-Security-Policy from a live response and grades it alongside every other header on this reference — no account needed.

For the longer treatment, read HTTP Security Headers: HSTS, CSP, X-Frame-Options & More in the Learning Center.

Related headers