A CSP that controls scripts
Your CSP may do nothing. A Content-Security-Policy header can be present and still let an injected script run, because the part that matters is the rule for scripts. The browser reads script-src, and falls back to default-src when there isn’t one. With neither, the policy leaves scripts alone.
Does yours control scripts?
These don’t control scripts:
frame-ancestors 'none'only says who can frame the page.upgrade-insecure-requestsonly rewriteshttplinks.default-src *lets any host serve a script.script-src https:lets any HTTPS host serve a script.script-src 'self' 'unsafe-inline'lets an injected inline script run.
This one does in part:
script-src 'self' 'unsafe-eval'loads scripts only from your site, but they can run strings as code.
These do:
script-src 'self'runs only your own files.script-src 'nonce-abc123' 'strict-dynamic'runs only scripts carrying this response’s nonce, and the scripts they load.
A policy sent as Content-Security-Policy-Report-Only controls nothing either. It only reports. Be careful with a shared host in script-src, such as a public CDN: if anyone can publish files there, an attacker can load one.
Fix it in four steps
1. See what the browser would block
Send your target policy as a report, so nothing breaks:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'
Open your main pages and watch the browser console. Each line it prints is something the policy would have blocked.
2. Deal with inline scripts
These are why most sites keep 'unsafe-inline'. You have three ways out:
- Move the script to a file. That is enough for code you wrote yourself. This includes
onclick=attributes, which a policy without'unsafe-inline'blocks. - Use a nonce on server-rendered pages. Generate a new random value for each response, and put it in both the header and each
<script nonce="...">. Never reuse one. In Next.js, this means generating it in middleware, and those pages can no longer be served as static files. - Use a hash on static pages. Put the SHA-256 of the script’s text in
script-src. It works with no server, and it changes each time the script changes.
3. Add what you really need
Add each third-party script by its exact host, such as your analytics provider. With a nonce, add 'strict-dynamic', and scripts you trust can load what they need.
4. Enforce it
When the console stays quiet, rename the header to Content-Security-Policy:
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'
What it cannot see
Today’s checks catch the worst cases, so read the header yourself with the rules above.
npx penthera <url>flags a policy that names no script sources, or lets any host run them with*,http:orhttps:. It doesn’t judge the rest of the policy.- The check on this site does the same, and also flags
'unsafe-inline'and'unsafe-eval'for scripts. - Both read one response from one page. Other pages can send other headers.
penthera fixadds a starter CSP that keeps'unsafe-inline', so frameworks don’t break. It’s a start. Finish it with the steps above.
A strict CSP is not a guarantee. It limits what an injected script can do, and the bug that let it in still needs fixing. A policy can also break a page, so test it first.
Check it
curl -sI https://your-domain.com | grep -i content-security-policy
npx penthera https://your-domain.com
curl prints the header the world sees. It controls scripts if it’s named content-security-policy, has script-src or default-src, and has no 'unsafe-inline', * or bare https: there. npx penthera reports a missing header, and runs the rest of a standard scan.
Or use the check on this site, which also reads your TLS, cookies and DNS, and looks for keys in your JavaScript with 24 secret patterns. Check only a site you own, as the terms say.
Check my siteFree, passive, nothing stored. Your own site only.