CSP: remove 'unsafe-inline' from scripts and kill XSS blast radius
A strong Content-Security-Policy turns a script injection into a non-event
The threat
With 'unsafe-inline''unsafe-inline'CSP keywordA Content-Security-Policy value that lets the browser run inline scripts or styles written directly in the page. In script-src it's the danger switch, it re-enables exactly the inline execution that injected XSS needs. Replace it with per-script hashes or a nonce. in your script-srcscript-srcCSP directiveThe CSP directive that controls which scripts a page may load and execute. It is the single most important directive for stopping cross-site scripting (XSS)., any successful script injection, reflected, stored, or DOM-based, executes with full page privileges: read cookies, keylog, make authenticated requests, exfiltrate data. A strong CSPCSPHTTP headerContent-Security-Policy: a response header that whitelists exactly which scripts, styles, images and connections a page may use, so an injected payload simply has no permission to run. is the seatbelt: even if an injection lands, the browser refuses to run unauthorized script. 'unsafe-inline''unsafe-inline'CSP keywordA Content-Security-Policy value that lets the browser run inline scripts or styles written directly in the page. In script-src it's the danger switch, it re-enables exactly the inline execution that injected XSS needs. Replace it with per-script hashes or a nonce. unbuckles it.
The exact fix
This is the one fix that needs care, removing 'unsafe-inline''unsafe-inline'CSP keywordA Content-Security-Policy value that lets the browser run inline scripts or styles written directly in the page. In script-src it's the danger switch, it re-enables exactly the inline execution that injected XSS needs. Replace it with per-script hashes or a nonce. can break inline scripts, so you authorize your scripts explicitly instead.
Option A, hash every inline script (what we did). At build time, compute the sha256sha256 hashCSP techniqueA fingerprint of an inline script's exact contents. Listing the hash in script-src authorizes that one specific script without needing 'unsafe-inline'. of each inline <script> block and add the hashes to script-srcscript-srcCSP directiveThe CSP directive that controls which scripts a page may load and execute. It is the single most important directive for stopping cross-site scripting (XSS).. The browser then runs only those exact scripts and nothing injected. For a real site this is automated in the build so every pageβs inline scripts are covered.
Option B, externalize. Move inline scripts into .js files in your bundle; then script-src 'self' (plus any needed external host) is all you need, no hashes to maintain.
Here is the hardened policy we shipped (Netlify), inline-script hashes included:
# netlify.toml
[[headers]]
for = "/*"
[headers.values]
Content-Security-Policy = "default-src 'self'; script-src 'self' 'sha256-β¦' 'sha256-β¦' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com wss://api.yourdomain.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"
X-XSS-Protection = "0"
Or the Netlify _headers form:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' 'sha256-β¦' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.yourdomain.com; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
What this policy already gets right: frame-ancestors 'none' (clickjacking-proof), base-uri 'self', form-action 'self'. The only change needed was the script context.
style-src 'unsafe-inline'is fine to keep. Inline style attributes (style="β¦") canβt be CSP-hashed and are low-risk. The XSSXSSAttackCross-Site Scripting: an attack that injects attacker-controlled JavaScript into your page to steal sessions, keylog, or act as the user. A strong CSP is the primary defense. danger lives in script-src, thatβs the one to lock. (We even corrected our own scanner to stop penalizing style-src for this reason.)
Verify it
curl -sI https://yourdomain.com | grep -i content-security
Confirm script-srcscript-srcCSP directiveThe CSP directive that controls which scripts a page may load and execute. It is the single most important directive for stopping cross-site scripting (XSS). contains no 'unsafe-inline''unsafe-inline'CSP keywordA Content-Security-Policy value that lets the browser run inline scripts or styles written directly in the page. In script-src it's the danger switch, it re-enables exactly the inline execution that injected XSS needs. Replace it with per-script hashes or a nonce. / 'unsafe-eval''unsafe-eval'CSP keywordA CSP value permitting eval() and other string-to-code execution. It lets an attacker turn plain data into running code, keep it out of script-src.. Then load the site with DevTools open, zero CSP violation errors means every legitimate script is authorized.
Proof
dev3lop.com hashed inline scripts across 1,341 pages at build time, removing 'unsafe-inline''unsafe-inline'CSP keywordA Content-Security-Policy value that lets the browser run inline scripts or styles written directly in the page. In script-src it's the danger switch, it re-enables exactly the inline execution that injected XSS needs. Replace it with per-script hashes or a nonce. from scripts entirely while keeping the site fully functional, a clean β10 on the road to A / 100.