The Broken-Widget Problem
Deploying a Content Security Policy is one of the strongest defenses against XSS attacks. But a single misconfigured directive can silently break inline scripts, block CDN resources, or disable analytics. If you apply CSP directly via the Content-Security-Policy header and a critical script gets blocked, your production site breaks—often without an obvious error console notification.
This is where Report-Only mode saves the day.
Report-Only vs Enforced Mode
CSP supports two headers:
| Header | Behavior |
|---|---|
Content-Security-Policy | Blocks violations immediately |
Content-Security-Policy-Report-Only | Logs violations without blocking |
With Report-Only, a disallowed script still executes but the browser POSTs a JSON violation report to a designated endpoint. You can audit every false positive before locking the policy down.
Setting Up Report-Only
Content-Security-Policy-Report-Only: default-src 'self';
script-src 'self' https://analytics.example.com;
report-uri https://your-site.example/csp-reports;
report-to csp-endpoint;
The report-uri directive (deprecated but widely supported) sends reports to a URL. The newer report-to directive uses the Report-To header to define an endpoint group. For maximum browser coverage, include both.
Receiving Reports
Each violation report is a JSON payload:
{
"csp-report": {
"document-uri": "https://example.com/page",
"blocked-uri": "https://evil.com/script.js",
"violated-directive": "script-src 'self'",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self'"
}
}
You can collect these via:
- A dedicated server route that logs to a file or database
- Third-party services like Report URI (report-uri.com)
- Cloud provider log analytics (AWS S3 + Lambda, Cloudflare Workers)
Gradual CSP Rollout Strategy
A safe rollout follows four phases:
Phase 1 — Monitor (Report-Only)
Apply Content-Security-Policy-Report-Only to a subset of traffic using a backend toggle or a reverse-proxy rule. Let it run for 1–2 weeks to capture all legitimate resource patterns.
# nginx: report-only for staging
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-reports;" always;
Phase 2 — Triage Reports
Analyze the collected reports. Common false positives include:
- Inline event handlers (
onclick,onload) that need'unsafe-hashes'or'nonce-...' - Third-party CDN scripts not listed in
script-src - WebSocket connections not covered by
connect-src
Phase 3 — Lock Down (Enforced)
Once the report stream stabilizes (zero unexpected violations for several days), switch to the enforced header. Keep Report-Only active with a stricter policy to explore further tightening.
Phase 4 — Iterate
Content-Security-Policy: default-src 'self';
script-src 'self' 'nonce-abc123';
report-uri https://your-site.example/csp-reports;
Content-Security-Policy-Report-Only: default-src 'self';
script-src 'none';
report-uri https://your-site.example/csp-reports;
Run a second Report-Only policy alongside the enforced one to test an even stricter future version without risk.
Key Directives for Report-Only
| Directive | Purpose |
|---|---|
report-uri | URL to receive violation reports (legacy) |
report-to | Endpoint group name from Report-To header |
script-src | Controls script execution |
style-src | Controls stylesheet loading |
img-src | Controls image sources |
connect-src | Controls fetch/XMLHttpRequest/WebSocket |
frame-ancestors | Controls iframe embedding |
Challenges with Report-Only
- Report-only fires once per violation per page load. After the first report, subsequent identical violations on the same page are silently ignored by most browsers.
- Some browsers send reports without a body. Filter out empty reports early.
- Report-Only cannot catch form submission target violations (
form-action). You need enforced mode to block those.
Summary
Content-Security-Policy-Report-Only is an essential tool for rolling out CSP without risk. It lets you observe violations in production, iterate on your policy based on real traffic, and only enforce when you are confident nothing will break. Pair it with a report-collection service, run it for at least a week, and graduate to enforced mode gradually.
