How HTTP Works

Response header

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

CSP Report-Only Header: Roll Out CSP Safely

Content-Security-Policy-Report-Only tests a policy without blocking. Rollout steps, report-uri vs report-to, sample violation JSON, and cutting noise.

Reviewed 2 min readadvanced4 sourcesTry itMarkdown
Direction
Response
Category
Security
Spec
W3C CSP Level 3
On this page

TL;DR: Content-Security-Policy-Report-Only runs a policy in observation mode: violations are reported to your endpoint, nothing is blocked. Ship it first, tune the policy against real traffic, then rename the header to Content-Security-Policy.

Rollout workflow

  1. Write the strict candidate policy (usually nonce-based, see Content-Security-Policy).
  2. Deploy it as Report-Only next to your existing policy, if any.
  3. Collect reports for a full release cycle, including rare pages (checkout, admin, embeds, error pages).
  4. Fix the first-party sources of violations: inline handlers, a missing CDN origin, eval. Add third parties deliberately, not wholesale.
  5. Reports drop to background noise: enforce, keep a Report-Only copy of the next stricter step.
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self' 'nonce-r4nd0m'; object-src 'none'; base-uri 'none'; report-to csp-endpoint
Reporting-Endpoints: csp-endpoint="https://reports.example.com/csp"

Caveats from the spec: Report-Only must be delivered as an HTTP header, not a <meta> element, and violations are still logged to the DevTools console without any reporting directive. A sandbox directive has no effect in Report-Only.

report-uri versus report-to

The old mechanism, report-uri, is deprecated but still the most compatible:

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

The reporting-API mechanism, report-to, names an endpoint declared in Reporting-Endpoints:

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

During migration send both directives. Browsers that support report-to ignore report-uri; the rest use it. Your collector then has to accept both formats.

What a report looks like

report-uri, POST with Content-Type: application/csp-report:

{
  "csp-report": {
    "document-uri": "https://example.com/signup",
    "referrer": "",
    "blocked-uri": "https://cdn.evil.example/lib.js",
    "effective-directive": "script-src-elem",
    "violated-directive": "script-src-elem",
    "original-policy": "default-src 'self'; report-uri /csp-reports",
    "disposition": "report",
    "status-code": 200
  }
}

report-to, POST with Content-Type: application/reports+json, an array of report objects. Field names are camelCase and the payload sits in body:

[
  {
    "type": "csp-violation",
    "age": 3,
    "url": "https://example.com/signup",
    "user_agent": "Mozilla/5.0 ...",
    "body": {
      "documentURL": "https://example.com/signup",
      "blockedURL": "https://cdn.evil.example/lib.js",
      "effectiveDirective": "script-src-elem",
      "originalPolicy": "default-src 'self'; report-to csp-endpoint",
      "disposition": "report",
      "statusCode": 200,
      "referrer": "",
      "sample": ""
    }
  }
]

disposition is report for Report-Only and enforce for the enforcing header. The sample field carries the first 40 characters of an inline script or style, but only when the policy includes the 'report-sample' keyword. Cross-origin blocked-uri values are truncated to scheme, host and port.

Keeping noise manageable

  • Treat reports as untrusted input. Anyone can POST to your endpoint, and fields such as script-sample are attacker-controlled; MDN says to sanitize before storing or rendering.
  • Drop blocked-uri values with chrome-extension, moz-extension or safari-extension schemes and reports whose source-file is an extension. They are the largest source of junk.
  • Deduplicate on (effective directive, blocked host, document path) and count, rather than storing every report.
  • Send the endpoint reports from a separate host or path with rate limiting so a hot page cannot knock it over. Hosted collectors exist, but check what URLs and user agents you are handing them.
  • Add 'report-sample' temporarily when you cannot work out which inline script is violating; remove it after because it increases data exposure.

Verify

curl -sI https://example.com | grep -i -E 'content-security-policy|reporting-endpoints'
curl -s -X POST https://reports.example.com/csp \
  -H 'Content-Type: application/csp-report' \
  -d '{"csp-report":{"blocked-uri":"inline","effective-directive":"script-src"}}' -i

The CSP evaluator is useful for checking the candidate policy before it ships.

Frequently asked questions

What does Content-Security-Policy-Report-Only do?

The browser evaluates the policy and reports violations, but blocks nothing. Pages behave exactly as before, so you can learn what a real policy would break across real users, then switch the header name to Content-Security-Policy to enforce it.

Can I use Report-Only and enforcing CSP together?

Yes. Send both headers: the enforcing one with your current safe policy, and the Report-Only one with the stricter candidate. Violations of each are reported with a disposition of enforce or report so you can tell them apart.

Should I use report-uri or report-to?

report-uri is deprecated but has the widest support, and report-to with a Reporting-Endpoints header is its replacement. MDN advises sending both during the transition: browsers that support report-to ignore report-uri, and the others fall back to it. The two use different body formats, application/csp-report versus application/reports+json.

Why is my Report-Only policy not sending any reports?

A Report-Only policy needs a reporting directive (report-uri or report-to); without one the violation shows only in the DevTools console. Also check that the endpoint is HTTPS, that the name in report-to matches a key in Reporting-Endpoints, and that the header is on the HTTP response, since Report-Only is not supported in a meta tag.

Why am I flooded with CSP reports from browser extensions?

Extensions inject scripts and styles that violate your policy, and they often show up with blocked URIs such as chrome-extension://, moz-extension:// or inline. Filter those schemes server-side, drop reports whose source file is not your origin, and sample or deduplicate before storing.

Sources

  1. MDN Web Docs: Content-Security-Policy-Report-Onlydeveloper.mozilla.org
  2. MDN Web Docs: CSP report-urideveloper.mozilla.org
  3. W3C Content Security Policy Level 3w3.org
  4. W3C Reporting APIw3.org

Keep going

Browse /search