Skip to main content
SecurityWeb DevelopmentDevOpsCSP

Rolling Out a Content-Security-Policy Without Breaking Your Site

ZsTechLabs Team·July 16, 2026·4 min read

A Content-Security-Policy is one of the highest-leverage security headers a business site can ship — it's the browser-level control that decides which scripts, frames, and network calls your pages are allowed to make, which is exactly the surface XSS and script-injection attacks live on. It's also the header most likely to break your own site if you enforce it carelessly, because modern business sites load a surprising amount of third-party code: analytics, chat widgets, CAPTCHA challenges, payment overlays. This post walks through the rollout path we use on our own site, including the part most guides skip — actually collecting violation reports before enforcing anything.

Start in Report-Only mode, but know what that does and doesn't give you

CSP ships under two header names: Content-Security-Policy (enforcing) and Content-Security-Policy-Report-Only (observing). Report-Only evaluates every page load against your policy and reports what would have been blocked, without blocking anything. That makes it the obvious first step: you write your best-guess policy, ship it under the Report-Only key, and let real traffic tell you what you missed.

The catch is where those reports go. By default, they go nowhere — violations appear in each visitor's browser console and vanish. If your rollout plan is "run Report-Only for two weeks, then enforce if it looks clean," you need to be honest about what "looks clean" means when nobody is collecting the data. We ran exactly this gap on our own site: the policy soaked in Report-Only mode for weeks, but until we added a collector, the only violations we could ever have seen were the ones we happened to trigger ourselves, on our own machines, with our own browsers and extensions. That's not a soak — that's a sample size of one.

Build the report collector before you plan the enforcement date

The fix is one small endpoint and one directive. Adding report-uri /api/csp-report to the policy tells browsers to POST a JSON description of every violation to your own server, where you can log and review it. The collector itself needs three properties:

Accept both report formats. Older report-uri reports arrive as a csp-report object with kebab-case keys (violated-directive, blocked-uri); the newer Reporting API sends an array of typed entries with camelCase fields. Handling both costs a few lines and saves you from silently dropping half your data depending on the visitor's browser.

Rate-limit it. A public endpoint that accepts arbitrary POSTs is a spam magnet, and a genuinely misconfigured policy can generate a report on every page load from every visitor. Cap reports per IP so a flood doesn't drown your logs or your disk.

Log fields, not payloads. The useful signal is the violated directive and the blocked origin. The full report can include complete URLs with query strings — which on some sites means tokens or personal data — so strip queries and log only what you'll actually act on.

The violations to expect from third-party scripts

Once reports flow, most of what you'll see falls into predictable buckets. Analytics and tag managers load secondary scripts from origins you didn't list. Chat widgets are the worst offenders: they commonly spawn WebSocket connections (wss://) and web workers from blob: URLs, and blob: workers are blocked by a strict default-src 'self' unless you explicitly allow them via worker-src. Payment providers and CAPTCHA services need frame-src entries for their overlays and challenge iframes, plus connect-src entries for the API calls those frames make back out.

This is why the report-collection phase matters more than getting the first draft of the policy perfect. You will not correctly guess every origin a chat widget connects to by reading its documentation. Real traffic through real browsers is the only reliable inventory.

When to flip to enforcement — and when not to

Our criteria for graduating a policy from Report-Only to enforcing is deliberately boring: a sustained window of collected reports containing nothing but noise (browser extensions injecting scripts show up constantly and are safe to ignore — the reports name origins like moz-extension://), with every legitimate violation either allowlisted or consciously accepted. If your deploy pipeline goes straight to production — ours does, via PM2 — treat the flip with the same caution as a schema migration: it's a one-line change that affects every page, every visitor, immediately.

Two things are not good reasons to flip early: an arbitrary calendar date ("it's been two weeks"), and console-checking your own site and seeing nothing. Both were tempting on our own rollout; both would have meant enforcing blind.

It's also fine to keep a policy in Report-Only mode indefinitely on lower-risk sites — an observed policy that you review is worth more than an enforced policy you had to gut with unsafe-* allowances until it stopped protecting anything.

Where this fits in your broader security posture

CSP is one layer. It pairs with the rest of the header set (HSTS, X-Content-Type-Options, frame controls), sensible WordPress hardening if that's your platform, and the boring fundamentals from our WordPress security checklist — updates, least-privilege admin accounts, and rate limiting on the endpoints attackers actually probe.

If you'd like a second pair of eyes on your site's security headers — or a development team that treats this as part of shipping, not an afterthought — get in touch and we'll review what your site currently sends and what enforcing would actually break.