A safe Content-Security-Policy rollout
Content-Security-Policy is the single most powerful header you can add — and the one most likely to take your site down if you do it wrong. I've seen a "quick CSP fix" from a pentest report block the site's own analytics, payment iframe, and half its images inside five minutes of going live. The header works exactly as designed. The rollout is what people get wrong.
Here's how to ship a real CSP without the 2am rollback.
What CSP does, in one paragraph
A Content-Security-Policy is a whitelist the browser enforces. You tell it which origins are allowed to supply scripts, styles, images, fonts, frames and connections, and the browser refuses anything else. Its headline job is stopping cross-site scripting: even if an attacker sneaks a <script> onto your page, it won't execute unless it matches your policy. That's a genuinely strong defense — which is why auditors and pentesters keep flagging its absence.
The one rule: never enforce first
This is the whole game. A live site pulls scripts and styles from more places than you remember — analytics, a chat widget, an A/B tool, embedded video, a font CDN, inline handlers a plugin injected years ago. Write a policy from memory, enforce it, and you'll block something that matters.
So you don't enforce. You start with the report-only twin of the header:
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint
In report-only mode the browser reports what would have been blocked but blocks nothing. You run this for one to two weeks on real traffic, watch the violations pile up, and now you have the actual list of everything your site loads — not the list you guessed. Then you widen the policy to allow the legitimate sources, fix the illegitimate ones, and only when the reports go quiet do you switch the header name to the enforcing Content-Security-Policy.
Report-only mode turns "I think I know what my site loads" into "here's exactly what my site loads." Skipping it is how you break production.
Where to send the reports
A report-only policy with nowhere to send reports is useless. Two directives do this:
report-to— the modern one, built on the Reporting API. It reached broad browser support across Chromium, Firefox and Safari in 2026, so it's finally the right default.report-uri— deprecated, but still the most widely supported in the field.
Include both for now — report-uri as the fallback, report-to for modern browsers — so you don't miss violations during the transition. Point them at an endpoint that just logs the JSON payloads; you'll read them by hand at first.
The inline-script problem (and how to not cop out)
The moment you write a strict policy, inline scripts and styles stop working — and most sites have them. The lazy fix is 'unsafe-inline', which reopens the exact XSS hole CSP exists to close. Don't. The real options:
- Nonces — generate a random token per request, put it on the header and on each trusted
<script nonce="…">. Best for dynamically rendered pages. - Hashes — put the SHA-256 hash of a static inline script in the policy. Best for content that never changes.
strict-dynamic— lets a script you've already trusted (via nonce or hash) load the scripts it needs, without you whitelisting every third-party domain. It's what makes a strict, nonce-based policy survive contact with real third-party tags.
A modern strict policy leans on nonces plus strict-dynamic rather than a long allowlist of hostnames, which is both safer and less brittle when a vendor changes its CDN.
A sane starting point
Begin report-only, tight, and loosen from the reports:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'nonce-RANDOM' 'strict-dynamic';
style-src 'self';
img-src 'self' data:;
object-src 'none';
base-uri 'self';
frame-ancestors 'self';
report-uri /csp-report;
report-to csp-endpoint
object-src 'none' and base-uri 'self' are cheap wins that close common bypasses. frame-ancestors 'self' stops clickjacking (it's the modern replacement for X-Frame-Options). Everything else you tune from what the reports tell you.
The part that bites later: CSP rots
Here's what nobody warns you about. You get the policy perfect, enforce it, celebrate. Three months later marketing adds a new tracking pixel, the browser blocks it, and either a feature silently breaks or someone quietly slaps 'unsafe-inline' on to "make it work" — and your protection is gone. A CSP isn't a set-and-forget header; it drifts every time someone adds a tool. This is the same failure mode I keep coming back to with security headers generally and with HSTS: the header is only protecting you while it's still correct.
The fix is boring and it works: check your live headers on a schedule, so a weakened or missing CSP shows up as an alert instead of a surprise. Our free site checker grades your CSP and the rest of your headers in seconds — start there, then keep an eye on it. (New to headers? The how-to-add-security-headers guide has copy-paste configs for Nginx, Apache and Cloudflare.)
Check your CSP and headers now
Perimeter grades your live Content-Security-Policy and the rest of your security headers — plus TLS, DNS and accessibility — and re-checks them so a weakened policy doesn't slip by. Free, no signup.
Monitor headers continuously → · Agency plan $29/mo, up to 25 domains