Lion Cipher Lab
Roll out a Content Security Policy safely: Report-Only mode, a violation-report collector, and triage before you enforce
- AppSec / DevSecOps
- Intermediate
- ~90 min
- Free · no account required
Why it matters
A Content Security Policy (CSP) that actually stops cross-site scripting is strict, and a strict policy breaks things: every inline script without a nonce, every onclick attribute, every third-party beacon you forgot about. Ship it to enforcement on day one and checkout stops working. Never ship it, and you have no protection. The way out is the report-only rollout. The browser applies your policy in a pretend mode, blocks nothing, and sends you a violation report for everything it would have blocked. You read the reports, fix your own code, allowlist the vendors you really use, and enforce…
What you'll learn
- explaining what Content-Security-Policy-Report-Only does (the browser reports what the policy WOULD block and blocks nothing) and why it is a rollout stage, never a defense
- wiring both reporting mechanisms on one response: the deprecated report-uri directive, and report-to plus a Reporting-Endpoints header, knowing that a browser which understands report-to ignores report-uri
- parsing both wire formats field-exactly: the legacy application/csp-report body ({"csp-report": {...}} with document-uri, blocked-uri, effective-directive, script-sample and friends) and the Reporting API application/reports+json body (a list of {type: "csp-violation", body: {documentURL, blockedURL, effectiveDirective, sample, ...}})
- hardening a report collector that anyone on the internet can POST to: content-type allowlist, size cap, batch cap, per-field truncation, per-client rate limit, query-string stripping, JSON-lines storage and an HTML-escaped admin view, each one tested with hostile input
- triaging hundreds of reports into a handful of decisions: normalize, de-duplicate, bucket by directive and blocked origin, drop browser-extension noise, rank by count, and decide ALLOWLIST, REFACTOR or INVESTIGATE per bucket
- deriving the next policy from the triage instead of from guesswork, re-running in report-only to see what remains, and only then switching to the enforcing Content-Security-Policy header
- measuring the payoff and the limits: in report-only every injection still executed (3 of 3), after enforcing none did (0 of 3), and enforcing the first draft on day one would have broken 190 legitimate loads
- the honest limits: report-only protects nothing, reports are best-effort and get lost, browsers batch and delay them, the query string is NOT stripped by the browser, and extension noise is partly unattributable
Steps in this lab
- The idea: let the browser tell you what your policy would break
- Set up a folder and check Python
- Model the policy and the toy shop
- Build the hardened report collector
- Serve the toy shop with both reporting mechanisms
- Generate the report corpus, and why it is synthetic
- Attack your own collector before an attacker does
- Write the triage: from hundreds of reports to a few decisions
- Phase 1: collect in Report-Only and triage
- Phase 2: refactor, re-run in Report-Only, see what is left
- Phase 3: flip to enforcing, and measure the difference
- How a pro runs this differently
- Honest limits
- Where this fits with the other browser-hardening labs