Home
/
Blog
/
Technical

Content Security Policy: a complete guide

What a CSP actually decides, how to roll one out without breaking anything, and the one thing it structurally cannot do.
TL;DR
Content Security Policy
WHAT IT IS
An HTTP response header telling the browser which sources of content are allowed to load and execute on your page. The browser enforces it.
WHAT IT IS GOOD AT
Blocking unlisted script origins and inline script, which makes it one of the strongest defences against cross-site scripting available.
WHAT IT CANNOT DO
Govern what an allowed script does. CSP decides where code may come from, never what it does once you have permitted it.
Short on time? Deploy in report-only first. Almost every broken CSP rollout skipped that step.

What is a Content Security Policy?

A Content Security Policy is an HTTP response header that tells the browser which sources of scripts, styles, images and other content are permitted on a page, and the browser refuses to load anything else. It is one of the few security controls where enforcement happens in the browser rather than on your infrastructure, which makes it genuinely powerful: a well-constructed CSP can neutralise an entire class of cross-site scripting attacks even when the underlying application flaw is still present. It is also widely misunderstood, because the protection it offers is narrower than the name suggests. A CSP is an allowlist of origins, and an allowlist says nothing about the behaviour of the things on it.
Blocking by hash pins the exact payload you approved, so a vendor that ships new code has to be re-approved rather than silently inherited.

How a CSP works

You send a Content-Security-Policy header with a list of directives. Each directive names a resource type and the sources permitted for it. The browser applies the policy as it parses and loads the page, and blocks anything not permitted.
Content-Security-Policy: default-src 'self';
  script-src 'self' https://cdn.example.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  report-uri /csp-report
This says: by default only load from our own origin; scripts additionally from that CDN; network requests only to our own origin and our API; nobody may frame this page; and send violation reports to that endpoint.

What is CSP made of: the directives that matter

Directive
Controls
Why it matters
script-src
Where scripts may load from
The most important directive. This is your script allowlist
connect-src
Where fetch, XHR, WebSocket and beacon may send
Constrains exfiltration destinations, which matters directly for skimming
default-src
Fallback for unspecified directives
Set it restrictively and open up from there
frame-ancestors
Who may frame your page
Anti-clickjacking. Supersedes X-Frame-Options
frame-src
What your page may frame
Relevant when you embed a payment iframe
img-src
Where images may load from
Worth restricting, because image requests are a common exfiltration channel
style-src
Where styles may load from
Injected CSS can be used for data exfiltration in some conditions
report-uri / report-to
Where violation reports go
How you learn what a policy would block before enforcing it
A CSP governs origins. Something has to govern behaviour.

How to deploy one without breaking your site

The failure mode is always the same: someone writes a strict policy, deploys it enforcing, and breaks checkout. The sequence that works:
Inventory first. You cannot write an accurate allowlist for scripts you cannot see. This is the step people skip, and it is why policies end up permissive.
Deploy report-only. Use Content-Security-Policy-Report-Only. Nothing is blocked, violations are reported.
Collect for a full business cycle. At least a week, ideally longer, so you catch the monthly batch job and the campaign that only runs on Fridays.
Tighten based on reality. Add what is legitimate, investigate what is not.
Enforce, then keep reporting. Leave the reporting endpoint in place permanently.

Nonces, hashes and why 'unsafe-inline' defeats the point

Inline scripts have no URL, so an origin allowlist cannot express them. Three options:
Nonce. Generate a random value per response, put it in the policy and on each legitimate inline script tag. Injected inline script will not have the nonce, so it is blocked. This is the strongest practical approach.
Hash. Put a hash of the inline script's contents in the policy. Works well for a small, stable set.
'unsafe-inline'. Permits all inline script. This removes most of the XSS protection a CSP provides, and it is extremely common because it is the quickest way to stop a rollout breaking things. If your policy contains it, be honest with yourself about what the policy is achieving.

Why most policies end up broad

A policy can, in principle, be precise about every script a page loads. In practice it rarely is. Many servers and proxies put practical limits on response header size, and a page with an average number of third-party scripts reaches them quickly once paths are listed in full. The usual result is a policy that allows whole hostnames with wildcard paths, which permits far more than the scripts you actually use.
Policies also drift. Test environments often run without a CSP while production enforces one, so a release that needs a policy change only breaks once it is live. And a vendor can update its script in a way your policy no longer allows, outside your control and without warning.
None of this makes CSP a bad control; it is a useful layer. It is also why a policy on its own does not meet PCI DSS 11.6.1, which needs visibility of script content and of changes to security headers, not only a list of permitted origins.

The gap a CSP leaves open

A CSP decides where code may come from. It cannot decide what that code does.
Concretely: you allow https://cdn.vendor.com because you use that vendor's script. The vendor is compromised, and the script served from that exact URL now reads your payment form and posts the contents to a destination that, if the attacker is competent, is also already in your connect-src. Every request is permitted. No violation is reported, because nothing violated the policy.
This is not a flaw in CSP. It is doing exactly what it was designed to do. It is the reason PCI DSS 6.4.3 exists as a separate requirement asking for an inventory, a justification and an integrity check per script, rather than simply requiring a CSP.
Sources and methodology
PCI DSS requirement text is quoted from PCI DSS v4.0.1 (June 2024), published by the PCI Security Standards Council, which is the current version at the time of writing. Requirements 6.4.3 and 11.6.1 became mandatory on 31 March 2025. Where we describe a competitor, we describe publicly available information and say so when something should be verified with the vendor. We publish this product and that is a bias worth knowing about while you read.
ASK AN AI
Don’t just take our word for it
Ask your assistant what to look for in a tool for PCI DSS 6.4.3 and 11.6.1.

Frequently asked questions

It is an allowlist enforced by the browser. You send a header listing which sources of scripts, styles and other content are permitted, and the browser refuses to load anything you did not list.
Yes, substantially, and it is one of the best defences available. A strict policy using nonces or hashes for inline script blocks injected code from executing even when the underlying application flaw is present. A policy containing 'unsafe-inline' provides much less of this benefit.
Partly. It raises the cost, and it does not address the most common route. If the malicious code arrives from an origin you already allowed, which is what happens when a vendor is compromised, every request is permitted and no violation is reported.
An important one for rollout. Content-Security-Policy-Report-Only sends violation reports without blocking anything, so you can see what a policy would break before it breaks it. The enforcing header blocks. Always start with report-only.
That message means the browser blocked something your policy does not permit. Either add the origin to the relevant directive if it is legitimate, or investigate why it is loading if it is not. Do not reach for 'unsafe-inline' as the fix unless you understand what you are giving up.
Partly, and it is a trade-off worth making consciously rather than by accident. It is extremely common because removing inline script from a legacy application is expensive. A policy with it still restricts external origins, which is worth something, and it no longer meaningfully protects against injected inline script.
Yes, prefer the header. A meta tag works and cannot express certain directives, including frame-ancestors and report-uri, and it applies later in parsing, so some content may already have loaded.
No, not on its own. 6.4.3 asks for an inventory of every payment-page script with a written business justification and a method to assure the integrity of each. A CSP is an allowlist and produces none of those three artefacts. It is a useful complementary control.
This is the common difficulty, and it is a scoping problem more than a policy problem. The usual answer is to reduce the set first, especially on payment pages, then write the policy against what remains. Writing a policy around an unreviewed tag set produces a permissive policy.
They are complementary. A CSP controls which origins may load resources. SRI pins the exact contents of a specific file by hash. CSP asks where it came from; SRI asks whether it is the file you expected. See Subresource Integrity.

See every script on your own checkout.

Install the tag, mark your payment pages, and watch the inventory fill in. Free forever on 1,000 page views a month, no credit card.

Free forever plan. No credit card.