Content Security Policy Report-Only Rollout
CSP is the header most likely to break your site if you guess. Report-only mode means you never have to guess.
Do this now
Ship a candidate policy under the Content-Security-Policy-Report-Only header.
Browsers evaluate it against every page load and report violations — but block nothing:
Content-Security-Policy-Report-Only: default-src 'self';
script-src 'self'; style-src 'self'; img-src 'self' data:;
object-src 'none'; base-uri 'self'; frame-ancestors 'none';
report-uri https://yourdomain.report-uri.com/r/d/csp/reportOnly
(One line in your server config; it's wrapped here for readability.) Don't hand-write the policy — our security headers generator builds a starting CSP for your stack and gives you the exact nginx, Apache or Cloudflare syntax to paste.
Why report-only first
An enforcing CSP that's wrong doesn't degrade gracefully — it blocks your payment provider's script, your web fonts, your analytics, sometimes your own stylesheets, all at once, for every visitor. Almost every "CSP broke production" story starts with someone deploying an enforcing policy that was written from memory of what the site loads rather than evidence.
Report-only inverts the risk. The browser runs the full policy evaluation on real traffic
— every browser, every page, every third-party tag your marketing team added last quarter —
and tells you what would have been blocked. Users notice nothing. You get an
inventory of everything your policy missed, generated by production itself. The header can
only be set server-side, by the way — Content-Security-Policy-Report-Only is
ignored in a <meta> tag, so this is a server-config change, not a
template change.
Step 1 — Draft a candidate policy
Start strict and let the reports tell you what to loosen — the reverse (start loose,
tighten later) never happens in practice. A sane baseline: default-src 'self',
explicit script-src and style-src, object-src 'none',
base-uri 'self', and frame-ancestors to control framing. The
generator outputs this baseline with
your known third-party origins added, in copy-paste syntax for your server.
The blocker you'll hit immediately is inline code: <script> blocks and
onclick= attributes fail a strict script-src 'self'. Resist the urge
to paper over it with 'unsafe-inline' — that keyword gives back most of the XSS
protection you came for. Move inline scripts into files where practical, and use nonces or
hashes for the ones that have to stay.
Step 2 — Stand up a report endpoint
Violation reports have to go somewhere, and this corner of the platform is honestly messy. There are two mechanisms:
report-uri— the original directive. Deprecated on paper, but supported everywhere and the one you can actually rely on today.report-to— the successor, built on the Reporting API. Cleaner design (batched, out-of-band delivery), but browser support is uneven — Chromium-based browsers support it; Firefox and Safari coverage has lagged.
The pragmatic move: send both directives, and treat report-uri as the one
doing the work. For the endpoint itself, a hosted service (report-uri.com has a free tier)
or a few lines on your own server both work — reports arrive as JSON POSTs. Expect volume:
a busy site can generate thousands of reports a day, most of them duplicates.
Step 3 — Deploy and triage for two to four weeks
With the report-only header live, violations stream in. Triage them into three buckets:
- Your own resources the policy missed — a CDN subdomain, an inline
style, a
data:favicon. These are policy fixes: add the origin or refactor the inline code. - Legitimate third parties — analytics, fonts, embeds. Decide deliberately whether each belongs in the policy; this step doubles as a supply-chain audit, and it's often the first complete list anyone's had of what the site loads.
- Noise — browser extensions injecting scripts, ISP proxies, random
probes. Reports with
blocked-urivalues likemoz-extensionor origins you've never heard of, appearing from a handful of clients. Ignore these; they're the main reason you shouldn't chase a report count of zero.
Give it real calendar time — two weeks minimum, a month if you have monthly billing or reporting flows that only run occasionally. A policy validated only against yesterday's traffic misses the code paths that run on the 1st.
Step 4 — Enforce, and keep a canary
When a full business cycle has produced no legitimate violations, rename the header:
Content-Security-Policy-Report-Only becomes Content-Security-Policy,
same value, keep the reporting directives. Violations are now blocked — and still reported,
which is your production alarm for regressions and your evidence log if someone actually
attempts XSS.
A pattern worth keeping afterwards: when you next want to tighten the policy, run the stricter candidate in a report-only header alongside the enforcing one. Browsers evaluate both independently, so you can trial every future change with zero risk, exactly like the first rollout.
Verify the header is actually out there
Config files lie — proxies strip headers, CDNs override them, a location
block shadows the one you edited. After deploying, run your site through our
security headers checker to see the headers your
server really sends, and use the generator
whenever you need the syntax for a policy change. If you're rolling out CSP for a client,
the Vuln Assessment Report Toolkit is a
ready-made template for writing up the before/after.
weekly kev brief
Patch what's actually being exploited.
One email a week: the vulnerabilities under active attack, prioritized — no noise.
Free. No spam. Unsubscribe anytime. Delivered by Beehiiv.