← Recipes

阅读中文版本

Roll Out a Content Security Policy via Report-Only

Deploy a strict CSP in report-only mode first, collect violation reports from real users, fix the causes, then enforce once reports fall silent.

~30 minsecuritywebheaders

Task

Introduce a strict Content Security Policy without breaking any page, using violation reports as the migration checklist.

When to use this

A first attempt at enforcing CSP broke inline scripts and checkout styling site-wide. The rollback took an hour and CSP got a reputation for being dangerous.

Prerequisites

  • A report collection endpoint, such as the report-uri directive or a reporting endpoint group
  • Access to server headers or meta tags
  • Real traffic to generate representative violation reports

Steps

1. Publish the policy in report-only mode.

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  report-uri /csp-report;

Expected output: Nothing is blocked yet; every would-be violation is reported to your endpoint with full context instead.

2. Collect and group the violation reports.

curl -s "https://api.example.com/csp-reports?since=24h" | jq -r '.[].blockedUri' | sort | uniq -c | sort -rn | head -10

Expected output: A ranked list of blocked resources shows exactly which third-party origins and inline patterns real pages still need.

3. Fix or allow each violation deliberately.

script-src 'self' https://analytics.example.com;

Expected output: Each report becomes either a code fix, moving the inline script to a file, or an explicit, minimal allow-list entry.

4. Wait for the report stream to go quiet.

curl -s "https://api.example.com/csp-reports?since=1h&unique=true" | jq 'length'

Expected output: Unique violations per hour trending to zero means the policy matches reality and is safe to enforce.

5. Switch the same policy to enforcement.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' https://analytics.example.com;
  report-uri /csp-report;

Expected output: The identical policy now blocks violations, and the reporting continues as a regression detector for future changes.

Troubleshooting

Problem

Report flood drowns the endpoint

Solution

Sample in the collector, deduplicate by document-uri plus blocked-uri, and keep raw volume capped; the signal is unique combinations, not volume.

Problem

Violations only appear on old cached pages

Solution

Cache headers let old HTML linger; set conservative caching on documents while migrating, and expect a long tail until caches expire.

Problem

Third-party script needs unsafe-inline

Solution

A vendor requiring inline scripts forces a choice: accept the risk explicitly with a nonce, or replace the vendor; do not silently weaken the whole policy for one widget.

Limitations

  • Report-Only cannot catch violations that only occur post-enforcement, such as code paths behind feature flags; keep reporting on after switching.
  • Report volume depends on traffic; low-traffic pages need targeted manual testing to validate the policy.

Enforce last, listen first

A strict CSP enforced on day one is a bet that every page, plugin, and third-party script already complies. Report-Only removes the bet: the exact policy ships in listening mode, real users generate the violation list, and each entry becomes a fix or a deliberate exception. Enforcement flips only when the report stream proves the policy matches reality, and reporting stays on as a permanent regression detector.

# Phase 1: listen
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; report-uri /csp-report;

# Phase 2: enforce the identical policy
Content-Security-Policy: default-src 'self'; script-src 'self'; report-uri /csp-report;

Reports are a checklist, not noise

Treat the violation stream as the migration backlog: deduplicate by document and blocked URI, rank by frequency, and burn the list down item by item. Each closed report is either a code improvement, inlining removed, or an explicit allow-list entry that someone signed off on. The list reaching zero is the enforcement gate.

Keep reporting after enforcement

The report-uri stays valuable forever: it catches the next deploy that adds an inline script, the next vendor that embeds an unapproved origin, and the next experiment that breaks the policy. Enforcement blocks them, and the report tells you why, turning every future violation into a named fix instead of a mysterious broken widget.