Skip to main content
All fixes

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 Audit

How to fix it

  1. 1

    Audit inline scripts and styles

    Crawl the production site and grep the rendered HTML for <script and <style tags without src= or href=. Use the CheckFast Full Site Audit at /check or run a one-off with wget -r --no-check-certificate https://example.com and a grep pass.

  2. 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. 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 the nonce prop. Rails: ActionView automatically injects csp_meta_tag and inline blocks via the helper. Migrate inline event handlers to addEventListener calls — they cannot carry a nonce.

  4. 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. 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. 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. 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