How to Roll Out Content-Security-Policy-Report-Only Safely
Content-Security-Policy-Report-Only is the header that lets you test a Content Security Policy against real traffic without actually blocking anything. Send it alongside your normal responses, wire up somewhere for the browser to send its findings, and you get a live feed of what your policy would have broken, before it breaks it. That's the whole pitch, and it's why almost every serious CSP rollout goes through this stage rather than skipping straight to enforcement.
This piece is not another rundown of what each CSP directive means. If you need that ground covered first, our guide to what each security header does is the place to start. Here, I'm assuming you already know roughly what you want your policy to say. What you're missing is the workflow: how to stand the reporting up, what the reports actually look like, how to tell real problems from noise, and when it's safe to flip the switch to enforcement.
What Content-Security-Policy-Report-Only Actually Does
Three layers are involved in any CSP rollout, and keeping them separate is what makes the rest of this article make sense. There's the server layer, which is just you deciding what header text to send. There's the browser layer, which reads that header on every page load and decides whether to enforce it, report on it, or both.
And there's the reporting layer, some endpoint that receives a JSON payload every time the browser sees a violation. Report-only mode only changes what happens at the browser layer.
Send Content-Security-Policy and a script from an unlisted domain gets blocked, full stop. The page breaks right there, in front of whichever visitor happened to load it. Send Content-Security-Policy-Report-Only instead, and the exact same violation gets logged and reported, but the script still runs.
Nothing on the page changes for the visitor. You get the diagnostic without the collateral damage. MDN describes this precisely: the header "helps to monitor Content Security Policy (CSP) violations and their effects without enforcing the security policies," which lets a team test or repair violations before deploying an actual enforced policy.
Think of it as training wheels for a policy you haven't proven yet. You're not disabling anything. You're just choosing to find out what would have broken before it actually does, on your terms, on your schedule.
Why not just ship an enforced policy and hope
I've watched teams skip this stage. It rarely goes well. A CSP with even one wrong source in script-src can blank an entire checkout flow, kill a payment widget, or silently drop an embedded video, and none of that shows up until a customer hits it (or, worse, until support tickets start arriving and nobody connects them to the header change from Tuesday).
Modern sites pull scripts, fonts, and iframes from a surprising number of third-party origins: analytics, chat widgets, payment processors, A/B testing tools, CDNs for a library someone added two years ago and forgot about. Writing a CSP that covers all of that correctly on the first try, from memory, is optimistic at best. Report-only mode replaces guessing with evidence.
Setting up a CSP reporting endpoint
Reports don't show up automatically just because you sent the report-only header. This trips up more people than you'd expect: they ship Content-Security-Policy-Report-Only, wait, and see nothing, because the header alone doesn't name a destination. Without a report-to directive pointing at a configured endpoint, the browser only logs the violation to that one visitor's own DevTools console. It goes nowhere near your servers.
Since September 2024, the modern way to wire this up has carried Baseline newly available status, meaning it works consistently across current Chromium, Firefox, and Safari. That's the report-to CSP directive paired with a separate Reporting-Endpoints header that names where reports should go. A lot of tutorials still ranking today were written before that milestone and either skip report-to entirely or treat it as experimental.
It isn't anymore, though the deprecated report-uri directive is still worth sending in parallel, since older clients that don't understand report-to will still honor it (and a browser that does understand report-to just ignores report-uri automatically, so there's no conflict in sending both).
A realistic header pair looks like this:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' https://cdn.example.com; report-to csp-endpoint; report-uri https://example.com/csp-reports
Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Note that this is a response-header-only mechanism. You can't deliver Content-Security-Policy-Report-Only through a <meta> tag the way a baseline enforced CSP can (with restrictions). If you're following a mixed guide and it suggests a meta tag for report-only, it's wrong. And if your draft policy includes a sandbox directive, know that it's silently ignored in report-only mode; there's no way to test sandboxing without actually enforcing it.
You'll want a collector on the receiving end that can parse a CSP violation report. Building your own is a reasonable weekend project (a POST endpoint that writes to a table is most of it), or you can point at a managed service that specializes in ingesting and deduplicating CSP reports. Either way, the endpoint needs to accept two different content types, because the two reporting mechanisms don't speak the same format.
Reading a CSP violation report
Here's where the format split actually matters. report-uri sends a single object as application/csp-report, using kebab-case field names:
{
"csp-report": {
"document-uri": "https://example.com/checkout",
"referrer": "https://example.com/",
"violated-directive": "script-src-elem",
"effective-directive": "script-src-elem",
"original-policy": "default-src 'self'; report-uri https://example.com/csp-reports",
"blocked-uri": "https://evil.example/inject.js",
"status-code": 200
}
}
report-to sends batched arrays of report objects as application/reports+json, using camelCase fields instead:
[
{
"age": 53531,
"type": "csp-violation",
"url": "https://example.com/checkout",
"body": {
"blockedURL": "https://evil.example/inject.js",
"disposition": "report",
"documentURL": "https://example.com/checkout",
"effectiveDirective": "script-src-elem",
"originalPolicy": "default-src 'self'; report-to csp-endpoint",
"referrer": "https://example.com/",
"statusCode": 200
}
}
]
Same underlying event, two different shapes. Your collector needs to handle both during the transition period, which realistically means indefinitely, since you're sending both directives on purpose. The fields you'll actually act on are the blocked URL (what got flagged), the effective directive (which part of your policy caught it), and the document URL (which page it happened on). Everything else is mostly useful for grouping and deduplication once the volume grows.
The browser extension noise problem
This is the part that catches almost everyone off guard the first time, and it's worth naming plainly instead of letting a reader discover it the hard way. Your reporting endpoint will start filling up with violations you didn't cause. Browser extensions inject their own scripts and stylesheets into pages, and those injections trip your CSP the same way a real attack would, because from the browser's point of view, it's the same kind of unlisted-source event.
Security researcher Troy Hunt has documented this directly: extensions and add-ons like Honey, RiteKit, UCBrowser, and Ciuvo inject their own scripts and styles into every page they run on, tripping CSP violation reports with zero connection to any actual vulnerability on the site being visited.
Dropbox's engineering team, writing about their own CSP reporting pipeline, reached the same conclusion from the receiving end: they built two layers of filtering, one for known browser-extension URI schemes and one for known third-party injector hosts, specifically because unfiltered noise from extensions and ad injectors made it impossible to see real signal in the reports at any meaningful scale.
In practice, extension noise shows up as violations pointing at blocked-uri values like chrome-extension://, moz-extension://, or safari-extension://. Filter those at the collector before they ever reach a human. Anything with that scheme in the blocked URL isn't a policy gap on your site; it's a browser extension doing what browser extensions do.
Once that noise is stripped out, what's left in your reports is almost always something genuinely worth looking at: a forgotten third-party script host, an inline handler someone added without a nonce, a CDN that changed its serving domain.
How long to run report-only
There's no single official number here, and I'd be skeptical of anyone who gives you one with total confidence. The honest answer depends on how many distinct templates and third-party integrations your site has.
For a typical site with a handful of page types, one to four weeks of report-only monitoring per major template is a reasonable target. That's enough time to catch traffic from different browsers, different regions, and the occasional user running an unusual extension or ad blocker, without dragging the rollout out indefinitely.
A common practical rule of thumb from CSP practitioners is to watch until genuine (non-extension) violations drop to a low, stable trickle, then treat that as your signal to move forward. It's a working heuristic, not a standard, so use it as a guide rather than a pass/fail test.
A large, template-heavy site with years of accumulated third-party integrations can reasonably need longer than a small brochure site, and the browser-extension noise problem described above doesn't shrink as the monitoring window grows, it just becomes more familiar, which is exactly why building filtering into the collector matters more than picking the "right" number of weeks. For most site owners, though, weeks is the right unit of time to plan around, not months or years.
Running report-only and enforced CSP together
Here's the part that surprises people who assume report-only and enforced CSP are an either/or choice: they're not. Browsers fully support sending both Content-Security-Policy and Content-Security-Policy-Report-Only on the same response at the same time, and OWASP's Content Security Policy Cheat Sheet documents this explicitly as a supported pattern.
The useful version of this looks like a looser enforced policy running alongside a stricter report-only one. The enforced header protects real users today, at whatever level of strictness you've already validated. The report-only header, meanwhile, tests a tighter version: one more directive removed, one more unsafe-inline closed off, frame-ancestors narrowed from a wildcard down to the handful of origins that actually need to embed the page (a change our comparison of X-Frame-Options vs frame-ancestors walks through in more detail), feeding you data on whether that next tightening step is safe to ship.
It turns CSP hardening into a continuous process instead of a single risky cutover, which matches how most mature security programs actually operate: incrementally, with evidence at each step, the same way a careful HSTS preload rollout is staged.
Worth saying plainly, because I've seen the opposite assumption cause real damage: leaving a policy in report-only mode forever is not a safer default. It feels safer, because nothing ever breaks. But nothing is ever blocked either.
A report-only policy with no enforced Content-Security-Policy behind it provides zero actual protection against the injection attacks CSP exists to stop. It's a monitoring stage, not a destination. Our own security headers checker flags exactly this pattern: a site sending only the report-only header scores as if it had no real CSP at all, because functionally, it doesn't.
Confirming the enforced header actually landed
Once you've watched the data long enough and you're ready to flip from report-only to enforced, the last step is verifying that the header your server sends is the header that actually reaches the browser. Load balancers, CDNs, and reverse proxies have a habit of dropping or duplicating headers somewhere along the chain, especially the first time a new header gets added to a config. If you want to check the full raw response your server is sending, independent of what your browser's DevTools show you, httpcheck.tools reads that response directly.
For the header itself, run the URL through our checker to confirm it's an enforced Content-Security-Policy landing, not a leftover report-only header still doing the work. If you're still assembling the directive list, the CSP generator builds both the report-only and enforced versions directive by directive, and outputs the block ready to drop into Apache, nginx, Caddy, Cloudflare, Netlify, or Vercel configuration.
Frequently Asked Questions
What does Content-Security-Policy-Report-Only do?
It tells the browser to evaluate a Content Security Policy and report any violations without actually blocking the content that triggered them. Scripts, styles, and other resources that would be blocked under an enforced policy still load normally; the browser just sends a report describing what would have been blocked. It's a monitoring tool, not a protective one, which is exactly why it's meant as a step before enforcement rather than a replacement for it.
Where do CSP violation reports go?
They go wherever you tell the browser to send them, and nowhere at all if you don't. The report-to directive, paired with a Reporting-Endpoints header naming the destination URL, is the current standard mechanism and reached Baseline newly available status in September 2024. The older report-uri directive points directly at a URL without needing the separate header, and is worth sending alongside report-to as a fallback for older browsers. Without either directive configured, violations only appear in that individual visitor's own browser console, never on your servers.
How long should I run report-only?
For most sites, somewhere between one and four weeks per major page template is a reasonable target, long enough to see traffic across different browsers and typical extension combinations. Treat that as a rough guide rather than a fixed rule: watch until genuine violations (after filtering out browser extension noise) settle into a low, stable trickle, and use that as your signal to move toward enforcement rather than committing to a specific date up front.
Can I run report-only and enforced CSP at the same time?
Yes. Browsers support sending both Content-Security-Policy and Content-Security-Policy-Report-Only headers on the same response, and OWASP's CSP Cheat Sheet documents this as a standard pattern. A common approach is an enforced policy set at a level you've already validated as safe, running alongside a stricter report-only policy that tests the next round of tightening. That gives you continuous protection from the enforced header while still gathering evidence for future changes from the report-only one.