SSL, TLS, security headers
Remove unsafe-inline from CSP and adopt nonce-based scripts
'unsafe-inline' negates most XSS protection a CSP would provide. Replace with per-request nonces or hashes plus 'strict-dynamic' for third-party scripts.
What's happening
A Content-Security-Policy directive containing 'unsafe-inline' permits inline and blocks, plus inline event handlers. Once allowed, the policy can no longer stop an injected script from executing — exactly the attack CSP exists to prevent. The header is still parsed, the page still scores points on naive scanners, but the XSS shield is gone.
Modern strict CSP replaces 'unsafe-inline' with a per-request nonce attribute on every legitimate inline block, optionally augmented by 'strict-dynamic' so scripts loaded by trusted bootstrap code are also allowed without a long source allowlist. Any inline content without a matching nonce is blocked.
The fix is operationally annoying — every inline and needs the nonce attribute generated server-side per request — but it converts CSP from a paper tiger into real defense-in-depth.
Why it matters
An XSS bug that would otherwise be high severity becomes much harder to exploit when CSP actually enforces script origins. With 'unsafe-inline' present, the attacker's injected runs without any obstacle.
Security audits and bug-bounty triagers accept 'unsafe-inline' as a finding even in the absence of a paired XSS bug, because it is a known control gap.
Mozilla Observatory and SecurityHeaders.com mark 'unsafe-inline' down significantly. SSL Labs does not test CSP but Google's CSP Evaluator (csp-evaluator.withgoogle.com) flags it explicitly.
Common causes
- Legacy templates use inline event handlers () that require 'unsafe-inline'.
- Server-rendered analytics pixels or A/B testing scripts inject inline tags after the page renders.
- A copy-pasted CSP from a tutorial that was never tightened.
- The framework's hot-reload bundle uses inline for the runtime in development and the dev policy leaked to production.
- Email-marketing platform injects unique inline tracking scripts per send and the team allowed 'unsafe-inline' to keep it working.
Detect this on your site
Run a quick scan with the Full Site Audit. The tool surfaces this exact issue with the records and context needed to apply the fix below.
Open Full Site AuditHow to fix it
- 1
Audit inline scripts and styles
Crawl the production site and grep the rendered HTML for
<scriptand<styletags withoutsrc=orhref=. Use the CheckFast Full Site Audit at /check or run a one-off withwget -r --no-check-certificate https://example.comand a grep pass. - 2
Generate a per-request nonce
In your web framework, generate at least 128 bits of cryptographically secure random per request and stash it in request-locals. In Next.js middleware:
const nonce = btoa(crypto.getRandomValues(new Uint8Array(16)).join('')). In Express:crypto.randomBytes(16).toString('base64'). - 3
Inject the nonce into every inline tag
Modify your template engine to add
nonce={request.nonce}on every inline script or style tag. React/Next.js: use thenonceprop. Rails: ActionView automatically injectscsp_meta_tagand inline blocks via the helper. Migrate inline event handlers to addEventListener calls — they cannot carry a nonce. - 4
Add strict-dynamic for third-party scripts
Replace any third-party allowlist with
'strict-dynamic'. The first-party nonced bootstrap script is trusted to load whatever it imports, transitively. This eliminates the need to maintain a list of analytics/ad/widget hostnames. - 5
Update the header and remove unsafe-inline
Set the header to
script-src 'nonce-{request.nonce}' 'strict-dynamic'(and the same pattern for style-src). No'unsafe-inline'. No'unsafe-eval'. Apply via web server or framework middleware on every response. - 6
Run in Report-Only first, then enforce
Roll the new policy in Report-Only mode and watch report-uri logs for a week. Fix any legitimate violations (typically still-inline tags missed in the migration). Once clean, switch to enforce.
- 7
Validate with CSP Evaluator
Paste the deployed header into csp-evaluator.withgoogle.com. The verdict should show no warnings for script-src or style-src. Add a CI test that fails if 'unsafe-inline' or 'unsafe-eval' reappears in the header.
Example
Content-Security-Policy: default-src 'self'; script-src 'nonce-2726c7f26c' 'strict-dynamic'; style-src 'nonce-2726c7f26c'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
Strict CSP without unsafe-inline, using nonces and strict-dynamic
Frequently asked
Yes. SHA256 hashes of inline content ('sha256-...' work for build-time-known inline scripts and avoid the per-request nonce generation. Many static-site generators support this. Use nonces for dynamic content and hashes for build-time content.
Only if a widget injects scripts via methods strict-dynamic does not propagate trust to (innerHTML on existing elements document.write). Most modern third parties use createElement+appendChild which strict-dynamic supports. Test in Report-Only first.
Related fixes
SSL, TLS, security headers
Deploy a Content-Security-Policy to mitigate XSS attacks
SSL, TLS, security headers
Block clickjacking with X-Frame-Options or CSP frame-ancestors
SSL, TLS, security headers
Add X-Content-Type-Options nosniff to block MIME sniffing
SSL, TLS, security headers
Set Referrer-Policy to control outbound Referer leakage