A CSP is one of the most effective protections you can add to a website, and also one of
the most misunderstood. This guide goes from what CSP actually does, all the way to
nonces, strict-dynamic, reporting and PCI-DSS 4.0.1. You don't need a security
background: if you can read an HTTP header, you can understand CSP.
# the simplest valid policy
Content-Security-Policy:
default-src'self';
# a more useful one
Content-Security-Policy:
default-src'self';
script-src'self'https://analytics.example.com;
style-src'self'https://fonts.googleapis.com;
font-srchttps://fonts.gstatic.com;
The basics
Part 1
what is a content security policy?
A guest list for the browser.
A Content Security Policy is a set of rules that tells the browser which resources your website is allowed to load, and just as importantly which to block.
A modern page loads a lot: scripts, stylesheets, fonts, images, analytics, chat widgets, payment forms. Each comes from somewhere: your own server, a CDN, a third party. A CSP is a guest list for those sources. If something tries to load from a source that isn't on the list, the browser refuses it.
Think of your website as a venue and the browser as the bouncer at the door. Without a CSP the bouncer lets anyone in. With a CSP you hand them a guest list: "Scripts? Only from my own domain and this one CDN. Everything else stays outside."
The policy is delivered as an HTTP response header your server sends with every page:
Content-Security-Policy:default-src'self'
That one line says: "By default, only load resources from my own origin." That's a complete, valid CSP.
why does it matter?
It defuses XSS and e-skimming.
The main threat CSP defends against is Cross-Site Scripting (XSS), and its modern, money-driven cousin, e-skimming (also called Magecart or formjacking). Somewhere on your site, untrusted input ends up rendered as HTML: a comment field, a search box, a URL parameter, a compromised third-party script. An attacker injects something like:
If that script runs, it can steal session cookies, read everything the user types (including card numbers on a checkout page), redirect them, or exfiltrate data. A CSP stops this: if your policy says scripts may only come from 'self' and no inline scripts are allowed, the injected <script> simply won't execute. The attacker got their code onto the page. The browser refused to run it.
For e-commerce the stakes are concrete. Card-skimming groups compromise a third-party script (analytics, a chat widget, an A/B testing tool) and use it to siphon card details from checkout. A CSP that only allows known, authorized scripts to run is one of the few controls that actually catches this. That is exactly why payment regulations now require it (see CSP and PCI-DSS 4.0.1).
your first csp header
Read it directive by directive.
Real sites load some outside resources. Say you use Google Fonts and a single analytics script. You'd extend the policy like this:
Content-Security-Policy:
default-src'self';
script-src'self'https://analytics.example.com;
style-src'self'https://fonts.googleapis.com;
font-srchttps://fonts.gstatic.com
default-src 'self' is the fallback: same origin only.
script-src: scripts may come from your origin or that analytics host.
style-src: stylesheets from your origin or Google Fonts' CSS host.
font-src: font files from Google's font host.
Each directive is separated by a semicolon; each has a name followed by a space-separated list of allowed sources.
Understanding the pieces
Part 2
A CSP is made of directives. Each directive governs one category of resource and is followed by a source list describing what's allowed.
The most common way developers "make CSP work" reopens the very door CSP is meant to close:
script-src'self''unsafe-inline'
'unsafe-inline' allows any inline script to run, including the one an attacker injected. You've added a CSP header, your scanner shows a checkmark, and you have almost no XSS protection. The two safe ways to allow the inline code you actually need are nonces and hashes.
nonce
Nonces: the recommended approach.
A nonce ("number used once") is a random value you generate on the server for each page load, put in the CSP header, and stamp on each inline <script> you trust.
The browser runs only inline scripts whose nonce matches the header value. An attacker can't guess it (it changes every request), so injected inline scripts are blocked while yours run fine.
hash
Hashes: for static inline code.
If an inline script never changes, allow it by the hash of its contents instead of a nonce. Compute the SHA-256 of the exact text between the tags, then list it:
Great for build-time-generated inline code where the content is fixed; awkward when content changes per request. That's what nonces are for.
strict-dynamic
strict-dynamic: scaling to real apps.
Host allowlists fall apart on complex sites: a trusted script loads another, which loads three more, each from a different CDN. 'strict-dynamic' (CSP Level 3) says: trust scripts loaded by an already-trusted script, regardless of host. You trust your entry-point script with a nonce or hash; anything it loads is trusted automatically, and the host allowlist is ignored.
CSP3 browsers see 'strict-dynamic' + the nonce and ignore 'unsafe-inline' and https:. Older browsers ignore 'strict-dynamic' and fall back to them. Strong policy on modern browsers, working fallback on old ones: the pattern Google recommends.
report-only
Report-Only mode: test before you enforce.
The most important operational feature in all of CSP. There are two headers:
Content-Security-Policy: enforces the policy. Violations are blocked.
Content-Security-Policy-Report-Only: observes. Violations are reported but nothing is blocked.
With report-only, the browser evaluates your policy against every resource, reports anything that would be blocked, and lets it all load anyway. Zero user impact while you collect complete data. The workflow that actually works:
Deploy a draft policy in report-only mode.
Collect violations across a week or two of real traffic.
Review what got flagged: legitimate resources you forgot, plus noise.
Adjust until the only violations left are things you genuinely want to block.
Switch the same policy to the enforcing header.
Keep monitoring. New violations mean an attack or an unannounced change.
Reporting
Part 4
A policy you can't observe is a policy you can't trust. Reporting is how CSP tells you what it's blocking, and it's the core of what csplog does. When the browser blocks (or, in report-only mode, would block) a resource, it sends a small JSON report to an endpoint you nominate, saying what was blocked, on which page, and which directive it violated.
report-uri vs report-to vs Reporting-Endpoints.
There are two mechanisms, and you currently need both. The legacy way, report-uri, POSTs each violation immediately as application/csp-report. It's technically deprecated, but has the broadest support and is the only mechanism Firefox honors as of 2026:
The modern way, the Reporting API, uses report-to, pointing at a named endpoint defined in a separate Reporting-Endpoints header. Reports arrive as application/reports+json with richer, camelCase fields:
Browser support hasn't fully converged. The universally-safe configuration specifies both directives pointing at the same endpoint, and this is exactly the snippet csplog hands you for a project:
Nobody warns you about this before you turn on reporting: most of what you receive is garbage. Browsers send violation reports for things that have nothing to do with your site. On a typical site, this noise outnumbers real violations. Teams turn on reporting, drown in junk within a day, and turn it off. It's the number one reason CSP reporting projects fail.
A specific directive replaces default-src entirely. Adding script-src 'self' does not inherit default-src's other sources. List everything explicitly.
default-src doesn't cover everything.frame-ancestors, base-uri and form-action do not fall back to it, so a default-src 'none' policy leaves all three wide open. And a browser only reports what your policy restricts, so an absent directive is silent rather than clean: leave form-action out and a form can post anywhere without ever producing a report. Set them explicitly.
'self' excludes subdomains.https://example.com does not match https://www.example.com. List subdomains or use *.example.com.
Inline event handlers count as inline scripts.onclick="…" is blocked unless allowed (via 'unsafe-hashes', discouraged). Move handlers into script files.
eval() needs 'unsafe-eval'. Some older libraries and setTimeout("string") calls rely on it. Find a modern alternative if you can.
data: images. Many sites need img-src 'self' data: for inline SVGs and base64 images. Fine for images; never put data: in script-src.
Third-party widgets pull in friends. A chat widget or tag manager loads scripts from hosts you never listed, exactly the case 'strict-dynamic' was designed for.
CMS and hosted platforms. WordPress, Shopify and similar often emit inline scripts you don't control. A pure nonce-based policy may be impractical; don't ship a broken site in the name of purity, but know exactly what you're trading away.
Not every report is a source you can allow. CSP has no way to express about:blank or a local file:/// path, so no line you add will permit them. Others are a decision about the directive rather than a host, such as whether img-src may take the bare https: scheme at all. Answering those as if they were hostnames fills an allowlist with entries that do nothing.
CSP and compliance: PCI-DSS 4.0.1.
If you take card payments and complete SAQ A-EP or SAQ D, two requirements aimed squarely at e-skimming apply to your payment page, and CSP is the usual way to meet them. PCI-DSS 4.0 (and the 4.0.1 revision) introduced them, in force since March 2025:
In plain terms: know which scripts run on your checkout, prove that only authorized ones run, and get alerted when your headers or scripts change unexpectedly. A CSP with violation reporting covers part of this: the policy authorizes which scripts may run, and violation reports surface when an unexpected script appears on a page someone visited.
A practical rollout strategy.
01
Baseline.
Deploy a starting policy in report-only mode, and make it default-src 'none'. Pre-approving nothing is the point: under 'self' your own scripts never violate, so they are never reported, and you finish with an inventory of other people's domains rather than a complete one. Give base-uri, form-action and frame-ancestors their own 'none' lines too. They do not inherit default-src, and a browser never reports what a policy does not restrict, so without those lines you stay blind to them however long you watch.
02
Observe.
Collect violations across real traffic for 7 to 14 days, and filter the noise. Judge over a window rather than over all time. A source that stopped appearing three weeks ago is not part of your site any more, and a window that is too short misses everything that only happens monthly, from an invoice page to a campaign script.
03
Refine.
Add the legitimate sources you discover. Decide your inline-script strategy: nonces for dynamic, hashes for static, 'strict-dynamic' for complex apps.
04
Ship it in report-only first.
Put the refined policy on the site under Content-Security-Policy-Report-Only before you enforce it. A quiet week under it is the only proof that holds. Any non-inheriting directive your refined policy does not name should still carry a 'none' line during this stage, or you stop hearing about it at the moment you are deciding. Those lines are probes rather than policy, so take them off when you enforce: a 'none' nobody chose would start blocking.
05
Enforce.
Move the refined policy to the enforcing header, without the probe lines. Nothing you actually decided should change on the way across. Keep object-src 'none', base-uri 'self' and frame-ancestors locked down, and say so explicitly now that the probes are gone.
06
Monitor forever.
New violations after enforcement mean one of two things: someone shipped a change, or someone is attacking you. Both are things you want to know immediately. Watch the header itself as well as the reports, because a deploy that drops your CSP, or quietly puts it back in report-only, produces no violations at all. Silence is not proof. Re-fetch your own pages on a schedule and compare what is served against what you shipped.
Reference
Part 6
Frequently asked questions.
Is a Content Security Policy required?
Not universally by law, but increasingly by regulation for specific cases. If you process card payments and complete SAQ A-EP or SAQ D, PCI-DSS 4.0.1 effectively requires it on your payment page. Outside compliance, it's a strongly recommended security best practice rather than a legal mandate.
Does CSP slow down my website?
No. CSP is enforced by the browser using a header you already send; it adds no network round-trips and negligible processing. Reporting sends small JSON payloads out-of-band, asynchronously, so it doesn't block page loads either.
Will CSP break my site?
It can, if you enforce a policy that's too strict before testing it. That's exactly why report-only mode exists: deploy in report-only first, see what would break, fix it, then enforce. Done in that order, the risk is minimal.
What's the difference between report-uri and report-to?
report-uri is the legacy directive: one POST per violation, application/csp-report format, broadest support, technically deprecated. report-to is the modern Reporting API directive: batched, application/reports+json, paired with a Reporting-Endpoints header. Support is still uneven, so the safe answer is to use both pointing at the same endpoint.
Can I use CSP on WordPress / Shopify / Squarespace?
Yes, though hosted platforms that emit inline scripts make a pure nonce-based policy harder. You can still get strong protection; you may need to allow 'unsafe-inline' in places, ideally with 'strict-dynamic' and nonces where the platform supports them. The key is knowing what you're allowing and why.
Do I need 'unsafe-inline'?
Usually not, and you should treat reaching for it as a red flag. Prefer nonces (for per-request inline code) or hashes (for static inline code). 'unsafe-inline' largely cancels out CSP's XSS protection, so use it only when a platform genuinely forces your hand, and monitor closely when you do.
Is CSP enough to prevent XSS on its own?
No. CSP is defense in depth: it limits the damage of an injection that already happened. You still need input sanitization, output encoding and dependency hygiene. CSP is the last line, not the only line.
What's a good report-uri alternative?
Any dedicated CSP reporting tool that filters noise and helps you act on the data, rather than just dumping raw reports. The features that matter are automatic noise filtering, a usable dashboard, and ideally help turning violations into an improved policy, which is what csplog is built around.
CSP cheat sheet.
Two policies, not one, because a rollout has two stages and they want opposite things. The first pre-approves nothing so that everything gets reported. The second allows what you found. Paste the first one today.
stage one: observe
Deny everything, block nothing, hear about all of it. Note the three lines after default-src: those directives never inherit it, and a browser stays silent about anything your policy does not restrict.
A hardened shape to fill in from what stage one reported. Add your real hosts per directive, and nothing you did not see. Ship it under Content-Security-Policy-Report-Only first, then move the same string to Content-Security-Policy unchanged.
csplog collects your violations, filters the noise, and assembles the header in code from exactly the sources you allowed. Nothing in it is something you did not decide.