# 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.

Source: https://howhttpworks.com/headers/content-security-policy-report-only
Last reviewed: 2026-10-04

> **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](https://howhttpworks.com/headers/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.

```http
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:

```http
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](https://howhttpworks.com/headers/reporting-endpoints):

```http
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`:

```json
{
  "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`:

```json
[
  {
    "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

```bash
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](https://howhttpworks.com/tools/csp-evaluator) is useful for checking the candidate policy before it ships.

## Related

- [Content-Security-Policy](https://howhttpworks.com/headers/content-security-policy), [Reporting-Endpoints](https://howhttpworks.com/headers/reporting-endpoints), [CORS vs CSP](https://howhttpworks.com/compare/cors-vs-csp)
