The pattern
A team adds CSP. Their app breaks. They add 'unsafe-inline' to script-src. The app works. Six months later the security review flags the CSP as effectively useless. They don't believe it.
They should.
What unsafe-inline actually does
It tells the browser: trust any inline <script> block, no questions. Including any script an attacker injects via XSS. The CSP becomes a thin compliance fig leaf.
The migration that always works
- Audit your inline scripts. Most will be analytics snippets, third-party widgets, or one-off bootstrap code.
- Add a nonce to every legitimate inline script:
<script nonce="{{ $nonce }}">. Generate the nonce per-request. - Update CSP to allow
'nonce-{{ $nonce }}'instead of'unsafe-inline'. - Test in report-only mode first —
Content-Security-Policy-Report-Onlywith a reporting endpoint catches anything you forgot. - Flip to enforcing.
What to do about third-party scripts
Most analytics vendors (Google Analytics, Plausible, Fathom) ship with a script that doesn't need inline. The third-party domains go in script-src. Inline tracking pixels typically don't need CSP relaxation — they're <img> tags.
What we look for in audits
When we review CSP, we check three things:
- Is
'unsafe-inline'present without nonce/hash fallback? Critical. - Is
'unsafe-eval'present? High — almost always avoidable with bundler config. - Are wildcard domains in script-src? Medium — supply-chain risk.
A real CSP closes about 70% of XSS exploit paths even when other layers fail.