Learn · Deployment
Report-Only vs. enforcement
The CSP rollout pattern that lets you ship a strict policy without breaking production: collect evidence, refine the allowlist, then switch from observe to enforce.
Why Report-Only exists
A Content Security Policy controls the browser's loading behavior, and the browser is unforgiving: an unallowed script just doesn't run. If your draft policy missed a vendor your checkout depends on, an enforced rollout silently breaks payments.
Report-Only is the spec's answer. The browser evaluates the policy and emits a structured report for every would-be violation — without blocking the load. You get the same visibility an enforced policy would give you, with none of the risk.
Reference: W3C Content Security Policy Level 3
Three headers, three roles
The two header names are spelled out below; the third row is the pattern of running them together during a transition.
Observability
Content-Security-Policy-Report-Only
The browser parses and evaluates the policy. Subresources that would have been blocked are still loaded — and a violation report is sent. Use this to assess impact before any user-facing impact.
Enforcement
Content-Security-Policy
The browser evaluates and blocks. If a report-to or report-uri endpoint is configured, violations are still reported — so you keep visibility while protection is on.
Iteration
Both at once
Send both headers simultaneously: enforce a known-safe baseline policy and Report-Only a stricter candidate. When the stricter one is quiet, promote it.
The rollout pattern
Six steps from first scan to a maintained, enforced policy.
- 1
Draft from observed behavior
Don't write a CSP from a wishlist. Run a browser-rendered crawl, see the actual scripts, styles, fonts, frames, and connect targets your pages load, and assemble the policy from that evidence.
- 2
Deploy in Report-Only mode
Ship the draft policy under the Content-Security-Policy-Report-Only header. The browser evaluates every directive but does not block — it only emits violation reports.
- 3
Collect violations from real traffic
Crawls miss what's behind login walls, payment forms, and personalization. Real user sessions surface the third parties and inline blocks the scanner couldn't reach.
- 4
Refine the allowlist
For every recurring violation, decide: is this an allowed source we missed (add it), an unwanted vendor (remove it from the site), or noise (ignore the path or set tighter rules).
- 5
Promote to enforcement
When violations stabilize at zero (or only known intentional ones), swap the header from Content-Security-Policy-Report-Only to Content-Security-Policy. The browser starts blocking.
- 6
Keep monitoring
Vendors update tags, marketing adds pixels, engineers add SDKs. Even after enforcement, alerting on new violations is how you catch drift before it becomes an incident.
Headers side by side
During the transition, send both. The Report-Only header carries the stricter candidate; the enforcement header keeps a known-safe baseline live.
# Currently enforced (known safe)
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com;
report-to csp-endpoint;
# Candidate strict policy under evaluation
Content-Security-Policy-Report-Only:
default-src 'none';
script-src 'nonce-{RANDOM}' 'strict-dynamic';
base-uri 'none';
object-src 'none';
report-to csp-endpoint-strict;When the strict policy reports zero unexpected violations across the traffic patterns you care about, swap headers — the enforcement policy becomes the strict one, and you can either retire Report-Only or use it for the next iteration.
Common pitfalls
Skipping Report-Only entirely. Going straight to enforcement on a real production site almost always breaks something — even if just a tag manager or a new vendor someone shipped last sprint.
Treating zero violations as 'done' too quickly. Crawls and Report-Only over a single weekend won't reveal seasonal flows, mobile-only paths, or low-traffic admin tools.
Ignoring report-to once enforcing. Violations after enforcement are how you discover vendor changes, supply-chain swaps, and accidental insecure code.
Including 'unsafe-inline' to avoid violations. That stays in production forever. Use a nonce or hash instead, even if it means a bit more wiring.