Why unsafe-inline Is a Problem
Let's be blunt: if your CSP includes 'unsafe-inline' in script-src, your CSP is barely doing anything. The whole point of Content Security Policy is to prevent unauthorized scripts from running. unsafe-inline tells the browser "go ahead and run any inline script you find," which is exactly what an XSS payload is — an inline script.
It's the most common CSP weakness we see when scanning sites. Developers add it because their site breaks without it, then never come back to fix it properly. Understandable, but it leaves a big hole.
Option 1: Nonces (Usually the Best Approach)
A nonce is a random string your server generates fresh on every page load. You put it in the CSP header and on each inline script tag. The browser checks that they match before executing the script.
Your CSP header:
Content-Security-Policy: script-src 'nonce-4f8a2b9c1e'
Your HTML:
<script nonce="4f8a2b9c1e">
// This runs because the nonce matches
</script>
The critical detail: the nonce must be different on every response. If you reuse nonces, an attacker who can observe one response can inject scripts with the same nonce. Most server frameworks have middleware or helpers that handle nonce generation automatically — use them.
Nonces are the best option for most sites because they work with existing inline scripts. You don't have to move everything to external files. Just tag your scripts with the nonce and you're done.
Option 2: Hashes (For Static Inline Scripts)
If you have inline scripts that never change — like a small analytics snippet or a config block — you can hash them. Take the SHA-256 hash of the script content and put it in your CSP:
Content-Security-Policy: script-src 'sha256-K7gNU3sdo+OL0wNhqoVWhr3g6s1xYv72ol/pe/Unols='
The browser hashes each inline script it finds and checks it against the allowlist. Matching scripts run, everything else gets blocked.
The downside: if the script content changes by even one character (including whitespace), the hash changes and the script breaks. This makes hashes impractical for dynamic inline scripts but perfectly fine for static ones.
Option 3: Move Everything to External Files
The cleanest solution, if you can do it. Move all your JavaScript out of the HTML and into .js files:
<script src="/js/app.js"></script>
Then your CSP just needs to allow your script origin — no nonces, no hashes, no unsafe-inline. This approach also makes your code more maintainable and cacheable.
The tricky part is inline event handlers. Things like onclick="doSomething()" count as inline scripts. You'll need to replace them with addEventListener calls in your JavaScript files. It's tedious for a large codebase but worth doing.
The Practical Path
For most teams, the realistic approach is: switch to nonces first (quick win, nothing breaks), then gradually move inline scripts to external files over time. Don't try to do it all at once — you'll get frustrated and give up. Ship the nonce-based CSP, then clean up incrementally.