Lion Cipher Lab
Lock down XSS with a strict nonce-based Content-Security-Policy
- AppSec / DevSecOps
- Advanced
- ~75 min
- Free · no account required
Why it matters
Almost every team that adds a Content-Security-Policy adds one that does nothing. They reach for a policy that includes 'unsafe-inline' -- because removing it breaks the site's inline scripts -- and 'unsafe-inline' is precisely the thing that lets an attacker's injected inline script run. So the header is present, the security scanner is happy, and the XSS still fires. A CSP that actually stops cross-site scripting looks different: it trusts scripts by an unguessable per-request nonce, not by where they come from, and it forbids the inline execution an attacker relies on. Google's own…
What you'll learn
- explaining what a Content-Security-Policy (CSP) is: an HTTP response header that tells the browser which sources of script, style, and other content it is allowed to load and run, so that even if an attacker injects markup, the browser refuses to execute it
- recognising the useless CSP everyone writes first -- one containing 'unsafe-inline' -- and explaining precisely why it provides essentially no XSS protection
- building the strong pattern instead: a per-response cryptographic NONCE on every legitimate <script>, with `script-src 'nonce-...' 'strict-dynamic'; object-src 'none'; base-uri 'none'`
- generating a fresh, unpredictable nonce for every single response and injecting it into both the CSP header and the script tags, and explaining why a static or reused nonce is worthless
- understanding 'strict-dynamic': how it lets a trusted (nonced) script load its own dependencies without you having to allowlist a list of hostnames -- and why host allowlists are bypassable in the first place
- refactoring an app so a strict CSP does not break it: moving inline event handlers (onclick=...) to addEventListener and inline styles out of the markup, because a strict policy blocks exactly those
- measuring the effect for real: an injected script beacons to a local listener; BEFORE the CSP the listener records 1 hit, AFTER the strict CSP the browser blocks the script (a CSP violation appears in DevTools) and the listener records 0 hits
- rolling CSP out the way professionals do: start with Content-Security-Policy-Report-Only plus a report collector so violations are REPORTED but nothing is blocked, fix what the reports reveal, THEN switch to the enforcing header
- the honest limits: CSP does not fix the underlying XSS (you still fix the injection); a nonce must be unpredictable and per-response; report-only blocks nothing so it is not protection on its own; older browsers ignore strict-dynamic and fall back to any host allowlist you include; and CSP cannot stop every injection outcome
Steps in this lab
- What a CSP is, and the trap almost everyone falls into
- Set up the lab app
- Start the beacon listener (your measurement instrument)
- BEFORE — build the app with no CSP and watch the beacon fire
- The strict policy, explained before you apply it
- ROLL OUT SAFELY — report-only first, with a collector
- AFTER — enforce, and measure the beacon go to zero
- Refactor the things a strict CSP breaks
- The honest limits — where a CSP still won't save you