CSP Evaluator

Paste a Content-Security-Policy header and find out what it really enforces. The interesting findings are rarely a syntax error: they are the directives that inherit nothing from default-src, and the tokens that quietly undo everything else in the policy.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Evaluate. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

unsafe-inline

The directive that makes the rest of the policy close to decorative against XSS

default-src 'self'; script-src 'self' 'unsafe-inline'

No default-src

Directives that do not fall back, so everything unlisted is unrestricted

script-src 'self'

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Keeping 'unsafe-inline' in script-src

    It permits exactly the injection CSP exists to stop, so the rest of the policy is close to decorative against XSS.

    Instead:Use a nonce or a hash. 'strict-dynamic' with a nonce is the modern shape.

  2. Setting only script-src

    Directives without a value do not fall back to script-src, only to default-src. Without default-src, object-src, base-uri and form-action are unrestricted.

    Instead:Set default-src 'none' and open up only what the page needs.

  3. Forgetting base-uri

    An injected <base> tag rewrites every relative URL on the page, including script sources, which routes around a script-src allowlist.

    Instead:Set base-uri 'self' or 'none'.

What it checks

A CSP is easy to write and hard to write well, mostly because the parts that fail are silent.

The directives that do not fall back

base-uri, frame-ancestors and form-action inherit nothing from default-src. Leave them out and they are unrestricted, however strict the rest of the policy looks. This is where most real CSP gaps are, and a policy can score well everywhere else while leaving all three open.

It knows when unsafe-inline is inert

In any browser that understands nonces, 'unsafe-inline' is ignored entirely when a nonce or hash is also present, so the same token is a critical hole in one policy and a harmless compatibility shim in another. Reporting both the same way would be a false positive, so this checks for the nonce before deciding.

Typos are reported, because browsers do not report them

An unrecognised directive is silently ignored. Write frame-ancestor instead of frame-ancestors and you have no clickjacking protection, no error in the console, and nothing anywhere to tell you. Every directive name is checked against the specification.

strict-dynamic, explained rather than scored

When 'strict-dynamic' is present, browsers discard every host and scheme source in that directive and trust flows from the nonce instead. That is the recommended modern shape, and it means the domain allowlist you are maintaining is doing nothing. The tool says so rather than quietly grading the list.

What a Content-Security-Policy actually does

CSP is a response header that tells the browser which resources this page is allowed to load and run. Its real purpose is narrow: it is a second line of defence for when your output encoding has already failed and an attacker has got markup onto the page. A good policy makes that injected markup inert.

Directives and sources

A policy is a list of directives separated by semicolons. Each names a resource type and the origins it may come from. Alongside real origins there are quoted keywords: 'self' for the page's own origin, 'none' for nothing at all, and the two that undo most of the protection: 'unsafe-inline' and 'unsafe-eval'.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none'

default-src is a fallback, not a default

It applies only to directives that are absent and that are defined to fall back to it. Fetch directives: script-src, style-src, img-src, connect-src and the rest: do fall back. Four important ones do not: base-uri, frame-ancestors, form-action and sandbox. Omitting those leaves them completely unrestricted no matter what default-src says, and that asymmetry is the single most common source of gaps in policies that otherwise look careful.

falls back to default-src     script-src, style-src, img-src,
                              connect-src, font-src, media-src,
                              object-src, frame-src, worker-src

does NOT fall back            base-uri
                              frame-ancestors
                              form-action
                              sandbox

Why base-uri matters more than it looks

A single injected <base> tag changes where every relative URL on the page resolves to. Your own script tags, written by your own templates and permitted by your own script-src, now load from the attacker's host. The policy appears to be satisfied the whole time. base-uri 'none' costs nothing and closes it, and it is left out of most policies.

<!-- injected -->
<base href="https://attacker.example/">

<!-- your own, unchanged, now fetched from the attacker -->
<script src="/js/app.js"></script>

Nonces beat allowlists

An allowlist of domains fails in two ways: any listed host with a JSONP endpoint or a vulnerable library version becomes a bypass, and the list grows until nobody dares remove anything. A nonce is a fresh random value per response, echoed on the script tags you intend to run. Injected script cannot carry it, so it does not execute: regardless of which origin it came from. Add 'strict-dynamic' and scripts loaded by trusted scripts inherit the trust, which is what makes the pattern workable with bundlers.

Content-Security-Policy:
  script-src 'nonce-r4nd0mP3rReQu3st' 'strict-dynamic'
             'unsafe-inline' https:;

<script nonce="r4nd0mP3rReQu3st" src="/app.js"></script>

'unsafe-inline' and https: are ignored by modern browsers here, they are only there for ones that do not understand nonces

It is a second line of defence, not the first

CSP does not fix an injection, it limits what the injection can do. Contextual output encoding, a templating engine that escapes by default, and validated input are still what prevent the hole. A strict CSP is what stops that hole becoming account takeover on the day one of them fails, which is why it is worth having even on an application you believe has no XSS.