May 28, 2026HeaderTest Team32 views

How to Deploy a Content Security Policy Without Breaking Your Site

CSP feels risky because a too-strict policy breaks your site. Report-Only mode lets you test against real production traffic and break nothing. This is the full rollout workflow, from first report to enforced policy.

Why CSP Feels Too Risky to Ship

Most teams know they should have a Content Security Policy. The reason they don't is simple: a CSP that's even slightly too strict breaks the site. A missing source in script-src and suddenly your analytics, your payment widget, or half your UI silently stops loading. Nobody wants to be the person who shipped the header that took down checkout.

So CSP sits on the backlog forever. The good news: the spec was designed for exactly this fear. There's a mode that lets you run your policy against real production traffic, see every single thing it would have blocked, and break absolutely nothing while you do it. It's called Report-Only, and it's how every serious CSP rollout should start.

Report-Only Mode: Evaluate Without Enforcing

Instead of sending the normal header, you send Content-Security-Policy-Report-Only. The browser evaluates your policy against everything the page loads, logs each violation to the console, and optionally sends a structured report to an endpoint you choose — but it never actually blocks anything.

Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-reports; report-to csp-endpoint

That single header change is the whole trick. You deploy it to production, your users notice nothing, and you start collecting a complete picture of what a real enforced policy would do to your traffic. No staging environment can give you this — real users load real third-party scripts, ad tags, browser extensions, and edge cases your test suite will never reproduce.

Wiring Up a Reporting Endpoint

Console warnings are fine for a quick look, but you can't watch every user's console. To collect violations at scale you need a reporting destination. There are two mechanisms, and in 2026 you should ship both.

The modern way: report-to and the Reporting API

First you declare a named endpoint with the Reporting-Endpoints response header, then you point the CSP directive at that name:

Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint

Chromium-based browsers batch these and POST them to your endpoint as application/reports+json. The body is an array of reports:

[
  {
    "age": 120,
    "type": "csp-violation",
    "url": "https://example.com/checkout",
    "body": {
      "documentURL": "https://example.com/checkout",
      "blockedURL": "https://cdn.thirdparty.com/widget.js",
      "effectiveDirective": "script-src",
      "originalPolicy": "default-src 'self'; report-to csp-endpoint",
      "disposition": "report"
    }
  }
]

Note "disposition": "report" — that's how you know this came from Report-Only mode rather than an enforced policy.

The legacy way: report-uri

report-to isn't supported everywhere yet — at the time of writing Firefox and Safari still rely on the older report-uri directive. So keep it as a fallback:

Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reports; report-to csp-endpoint

report-uri sends one POST per violation with content type application/csp-report and a slightly different body shape:

{
  "csp-report": {
    "document-uri": "https://example.com/checkout",
    "violated-directive": "script-src",
    "blocked-uri": "https://cdn.thirdparty.com/widget.js",
    "original-policy": "default-src 'self'; report-uri /csp-reports"
  }
}

Browsers that understand report-to will prefer it and ignore report-uri, so sending both doesn't double your reports — it just widens browser coverage. Build your endpoint to accept both formats.

Reading the Reports Without Drowning

The first time you turn this on, brace yourself: the reports are noisy. A large chunk of what lands in your endpoint will be junk you should never act on:

  • Browser extensions. Ad blockers, password managers, and translation tools inject inline scripts and styles into the page. These trip your policy constantly and have nothing to do with your code. A blocked-uri of inline, eval, or a chrome-extension:// / moz-extension:// origin is almost always this.
  • Injected network junk. Some ISPs and mobile carriers rewrite pages to inject scripts. You'll see violations from domains you've never heard of.
  • Duplicate spam. One broken page loaded by a lot of users generates a lot of identical reports.

The practical move is to aggregate, not read line by line. Group reports by effectiveDirective + blockedURL and rank by count. The handful of your own domains and legitimate third parties (your CDN, analytics, payment provider, fonts) float to the top. Those are your real work list. Everything in the long tail is noise you filter out.

From Report-Only to Enforcement

Here's the workflow that gets you to an enforced policy without a single outage:

  1. Start with a realistic draft. Don't begin with default-src 'none' — you'll get tens of thousands of reports and learn nothing. Start from something close to reality, like default-src 'self' plus the obvious third parties you already know about.
  2. Ship it Report-Only and wait. Give it at least a week. You want a full traffic cycle — weekday and weekend, every page, every checkout path, every locale.
  3. Triage the aggregated reports. For each legitimate blocked source, decide: add it to the policy, or remove the dependency. This is also a free third-party audit — you'll discover scripts nobody remembers adding.
  4. Tighten and repeat. Update the policy, redeploy Report-Only, watch the new reports. Each round the legitimate-violation count drops toward zero.
  5. Flip to enforcement. When the only things left are extension noise, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Now it blocks — and you already know it won't break anything real.

The Pro Move: Run Both Headers at Once

CSP lets you send an enforced policy and a Report-Only policy on the same response. This is how you keep iterating after launch with zero risk. Enforce the policy you trust today, and use Report-Only to test the next, stricter version against live traffic:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-{RANDOM}'

Your users are protected by the enforced policy right now, while you collect data on whether dropping the CDN allowlist in favor of nonces would break anything. When the Report-Only reports go quiet, you promote it to the enforced header with full confidence. You can run this loop forever.

A Few Things That Will Trip You Up

  • Report-Only is not protection. While you're in Report-Only mode, you have no CSP enforcement at all. It's a measurement tool, not a security control. Don't leave a site in Report-Only-only forever and think you're covered.
  • An endpoint that returns errors loses reports. Make sure your collector responds quickly with a 2xx and handles bursts. If it 500s or times out, those violations are gone.
  • Reports can leak URLs. document-uri and referrer fields may contain query strings with sensitive data. Treat your reporting endpoint as a place that receives PII and lock it down accordingly.
  • It only works over HTTPS in practice. The Reporting API requires a secure context. That shouldn't be a problem in 2026, but it catches people testing on plain HTTP locally.

The Short Version

You never have to gamble with a CSP again. Ship it as Content-Security-Policy-Report-Only with a reporting endpoint, watch real traffic for a week, fix the legitimate violations, and only then flip it to enforcement. Then keep a Report-Only header running permanently to test each future tightening before it goes live. The fear of "what will this break" is the whole reason CSPs don't get deployed — and Report-Only mode answers that question for free, in production, without breaking a thing.

Not sure what your current policy actually blocks, or whether you have one at all? Scan your site with HeaderTest to see your CSP, every security header you're missing, and a prioritized list of what to fix first — then set up ongoing monitoring so a future deploy never silently weakens it.

Topics

CSP Report-OnlyContent-Security-Policy-Report-Onlyreport-toreport-uriReporting APIdeploy CSPCSP rolloutCSP violations

Check Your Website's Security

Use our free scanner to analyze your CSP and security headers.

Scan Now - Free
How to Deploy a Content Security Policy Without Breaking Your Site | HeaderTest