CSP report-only mode is a testing mode for Content Security Policy that logs policy violations without blocking anything. You enable it by sending the Content-Security-Policy-Report-Only response header instead of the enforcing Content-Security-Policy header. The browser evaluates every script, style, image, and connection against your policy exactly as it would in enforcement mode — but when a resource would be blocked, the browser lets it load and instead POSTs you a JSON violation report. The result: you get a complete list of everything your policy would break, gathered from real users, before it breaks anything for real. Once the reports are clean, you switch to the enforcing header and the same policy starts actually protecting the page.
That is the summary an AI overview will give you. Here is what it can't: report-only mode is not a security setting — it provides zero protection — and the reporting directive most guides still show you (report-uri) is deprecated. Below is how the mode actually works, a side-by-side of the two headers, and the modern reporting setup that keeps working in current browsers.
The report-only rollout: log, tune, enforce
The whole point of report-only mode is to turn a risky "flip the switch and hope" deployment into a safe, evidence-driven loop. You start with a policy in report-only mode, watch the violations roll in, tighten the policy until the reports are clean, and only then enforce it.
How Report-Only Mode Works
When you use report-only mode, the browser doesn't block any resources that violate the policy. Instead, the browser reports the violations through the reporting mechanism you've configured.
The main use case is testing: you can deploy a CSP in report-only mode, monitor violations, refine the policy, and only switch to enforcement when confident the policy won't break functionality.
Content-Security-Policy vs Content-Security-Policy-Report-Only
Both headers use identical policy syntax. The only difference is what the browser does when a resource violates the policy — and that difference is everything.
| Aspect | Content-Security-Policy (enforce) | Content-Security-Policy-Report-Only (test) |
|---|---|---|
| Response header name | Content-Security-Policy | Content-Security-Policy-Report-Only |
| Violating resource | Blocked — does not load or execute | Allowed — loads and runs normally |
| Sends violation reports | Yes (if a reporting directive is set) | Yes (if a reporting directive is set) |
| User-facing impact | Page can break if policy is too strict | None — nothing changes for the user |
| Security protection | Yes — actually stops XSS / injection | No — monitoring only, zero protection |
disposition field in report | "enforce" | "report" |
| Requires a reporting endpoint | Optional (blocking works without one) | Effectively required (reports are the whole point) |
| Primary use | Production defense | Safe rollout, testing, tightening a live policy |
The takeaway: report-only is for learning what a policy does; enforcement is for doing what a policy promises. A policy that lives forever in report-only mode looks secure in your config but protects nothing.
Implementing Report-Only Mode
Setting the Header
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'unsafe-inline'; report-uri /csp-report
In various server technologies:
Express.js:
const helmet = require('helmet');
const express = require('express');
const app = express();
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'"],
imgSrc: ["'self'", "https:"],
reportUri: "/csp-report"
},
reportOnly: true // Report-only mode
}));
Nginx:
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https:; report-uri /csp-report;";
Apache:
Header add Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https:; report-uri /csp-report;"
Setting Up Violation Reporting
Report-only mode is most valuable when you capture and analyze violation reports.
Basic Reporting with report-uri
Specify where violations should be reported:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'unsafe-inline';
report-uri /csp-report
When a violation occurs, the browser POSTs a JSON report to /csp-report:
{
"csp-report": {
"document-uri": "https://example.com/page",
"violated-directive": "script-src",
"effective-directive": "script-src",
"original-policy": "default-src 'self'; script-src 'self' 'unsafe-inline'; report-uri /csp-report",
"disposition": "report",
"blocked-uri": "https://cdn.example.com/analytics.js",
"status-code": 200
}
}
Handling CSP Reports
Express.js endpoint:
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
const violation = req.body['csp-report'];
console.log('CSP Violation Report:', {
documentUri: violation['document-uri'],
violatedDirective: violation['violated-directive'],
blockedUri: violation['blocked-uri']
});
// Store violations in database for analysis
CSPViolation.create({
documentUri: violation['document-uri'],
violatedDirective: violation['violated-directive'],
blockedUri: violation['blocked-uri'],
originalPolicy: violation['original-policy'],
timestamp: new Date()
});
res.status(204).send(); // No content response
});
Modern Reporting: report-to and Reporting-Endpoints
Important: the report-uri directive shown above is deprecated. CSP Level 3 replaces it with the report-to directive, which does not take a URL directly — it references a named endpoint that you define separately in the Reporting-Endpoints HTTP response header.
Reporting-Endpoints: csp-endpoint="https://example.com/csp-report"
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
report-to csp-endpoint
The catch is browser support: browsers that understand report-to ignore report-uri, but some browsers still only understand report-uri. The pragmatic answer is to send both directives so every browser can report:
Reporting-Endpoints: csp-endpoint="https://example.com/csp-report"
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
report-uri /csp-report;
report-to csp-endpoint
Note that an older Report-To header (JSON group syntax) also existed to define endpoints; it is itself being superseded by the simpler Reporting-Endpoints header shown here. One more wrinkle: reports delivered via report-to use the newer Reporting API JSON shape (a body object), which differs slightly from the legacy csp-report payload — your endpoint should be prepared to parse both.
Build and Preview a Policy
You don't have to hand-write these directives from scratch. Use the generator below to assemble a policy, then deploy it under the Content-Security-Policy-Report-Only header first before enforcing it.
Third-Party Reporting Services
Rather than implementing your own reporting, use services like:
- Report-uri.com: Dedicated CSP reporting service
- Sentry: Error tracking with CSP support
- Bugsnag: Crash reporting with CSP reporting
Report-uri.com example:
Content-Security-Policy-Report-Only:
default-src 'self';
report-uri https://report-uri.com/r/d/csp/reportOnly
Testing Workflow
Implementing CSP effectively follows a structured approach:
Step 1: Deploy Lenient Report-Only Policy
Start with a lenient policy in report-only mode:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'unsafe-inline' https:;
style-src 'self' 'unsafe-inline' https:;
report-uri /csp-report
This policy is permissive but still identifies major issues.
Step 2: Monitor Violations
Collect violation reports for a period (days or weeks depending on traffic):
// Count violations by type
SELECT violated_directive, COUNT(*) as count
FROM csp_violations
GROUP BY violated_directive
ORDER BY count DESC;
// Identify problematic resources
SELECT blocked_uri, COUNT(*) as count
FROM csp_violations
WHERE violated_directive = 'script-src'
GROUP BY blocked_uri
ORDER BY count DESC;
Step 3: Whitelist Necessary Resources
Based on violation reports, add trusted resources to your policy:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com https://analytics.google.com/analytics/web/;
style-src 'self' https://fonts.googleapis.com;
report-uri /csp-report
Step 4: Tighten the Policy
Remove overly permissive directives:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com https://analytics.google.com/analytics/web/;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' https:;
report-uri /csp-report
Step 5: Switch to Enforcement
Once violations are resolved, switch to enforcement mode:
Content-Security-Policy:
default-src 'self';
script-src 'self' https://cdn.example.com https://analytics.google.com/analytics/web/;
style-src 'self' https://fonts.googleapis.com;
img-src 'self' https:;
report-uri /csp-report
Note: Use both headers during transition for additional safety.
Running Both Headers Simultaneously
Best practice during transition is running both headers:
Content-Security-Policy: [enforcement policy]
Content-Security-Policy-Report-Only: [stricter policy for testing]
This approach:
- Enforces your current policy
- Tests a stricter policy in report-only mode
- Identifies issues before enforcement
- Allows gradual policy tightening
Example transition:
# Current enforced policy (permissive)
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https:
# Testing stricter policy (report-only)
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://trusted-cdn.com;
report-uri /csp-report
Users experience the enforced policy, while the stricter policy is tested safely.
Common Report-Only Patterns
Initial Exploration
Content-Security-Policy-Report-Only:
default-src 'self' https:;
report-uri /csp-report
Very permissive, identifies major issues.
Testing Stricter Version
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' https://cdn.example.com;
style-src 'self' https://fonts.googleapis.com;
report-uri /csp-report
More restrictive, tests specific resource whitelisting.
Testing No Inline Scripts
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self';
style-src 'self' https:;
report-uri /csp-report
Very strict, identifies inline script dependencies.
Analyzing Violation Reports
Common Violations and Solutions
Inline Scripts:
{
"violated-directive": "script-src",
"blocked-uri": "inline"
}
Solution: Remove inline scripts or use nonces/hashes:
<script nonce="random-nonce">
console.log('Allowed');
</script>
Third-Party Analytics:
{
"violated-directive": "script-src",
"blocked-uri": "https://analytics.google.com/analytics.js"
}
Solution: Whitelist analytics domain:
script-src 'self' https://analytics.google.com
Google Fonts:
{
"violated-directive": "font-src",
"blocked-uri": "https://fonts.gstatic.com/s/..."
}
Solution: Add font-src directive:
font-src 'self' https://fonts.gstatic.com
Best Practices for Report-Only Mode
- Start permissive: Begin with lenient policies to identify what needs whitelisting
- Monitor consistently: Regularly review violation reports
- Use reporting service: Don't handle raw reports; use dedicated services
- Set time limits: Plan specific periods for testing (e.g., 2 weeks)
- Iterate gradually: Tighten policies incrementally
- Document decisions: Explain why each domain is whitelisted
- Test cross-browser: Violations may vary by browser
- Archive reports: Keep historical violation data for analysis
Production Recommendations
Once in production:
- Monitor violation reports continuously
- Alert on unexpected new violations (potential attacks)
- Maintain both enforcement and report-only during testing phases
- Have a response plan for CSP violation patterns
- Regularly review and tighten CSP
Limitations of Report-Only Mode
- Doesn't provide actual protection (only monitoring)
- Report delivery isn't guaranteed (can be blocked by network)
- Some browsers have CSP report limitations
- Report-only adds header size overhead
- Requires active monitoring to be valuable
Report-only mode is an essential tool for safely implementing Content Security Policy. By using it to test policies before enforcement, you can significantly reduce the risk of accidentally breaking website functionality while still improving security. The key is active monitoring and iterative refinement based on violation reports.