Article

CSP 'unsafe-inline': Why It Neutralises Your Policy and How to Remove It

'unsafe-inline' is a Content-Security-Policy keyword that lets every inline script run. In script-src it cancels CSP's main protection against injected scripts, because an attacker's payload is inline too. Replace it with a per-response nonce or a hash plus 'strict-dynamic', rolled out in Report-Only mode first.

By Paul Rudenko, Security ResearcherUpdated Sep 18, 20267 min read

'unsafe-inline' is a Content-Security-Policy source keyword that tells the browser to run every inline <script> block and event handler on the page. In script-src it cancels the main reason a CSP exists — stopping injected scripts — because an attacker's payload is inline too. The fix is a per-response nonce or a hash, plus 'strict-dynamic'.

Why does 'unsafe-inline' exist at all?

Before CSP, every site mixed HTML and JavaScript freely: onclick="…" attributes, analytics snippets pasted into templates, a <script> block that sets a config object. CSP's core idea is that the browser should only run code from places you named. Inline code has no “place” — it's just text inside the document — so a strict policy blocks it. 'unsafe-inline' is the escape hatch that switches that protection off so an older site keeps working. The name is honest: the W3C spec itself says it “should be avoided in favor of nonces or hashes” (CSP Level 3, §8.1).

Why does it neutralise the policy?

Cross-site scripting works by getting the browser to execute text an attacker controlled: a comment field rendered without escaping, a search term reflected into the page, a compromised template. That text arrives as inline script. If your policy says inline script is fine, the injected script runs exactly like your own. A script-src that allows 'unsafe-inline' still restricts external script hosts, but the attacker doesn't need one. Google's analysis of real-world policies found that the overwhelming majority of CSPs that used host allow-lists were bypassable, and 'unsafe-inline' was the most common reason (Weichselbaum et al., CCS 2016). That's why the CSP Checker reports it as a single MEDIUM finding rather than a cosmetic warning — and why it does not stack it into two findings when 'unsafe-eval' is also present: one weakness, one fix.

Nonce, hash or 'strict-dynamic' — which one?

Three tools replace 'unsafe-inline'; pick by how your pages are generated.

  • Nonce — the server generates a random, unguessable token for every response, puts it in the header as 'nonce-abc123' and on each legitimate inline script as nonce="abc123". Injected code can't know the token, so it won't run. Best for server-rendered pages (Next.js, Django, Rails, PHP).
  • Hash — you compute the SHA-256 of each inline script's exact content and list it as 'sha256-…'. No server logic needed, but every character change breaks the hash. Best for static sites with a handful of unchanging snippets.
  • 'strict-dynamic' — used with a nonce or hash. It says: any script that a trusted (nonced) script loads is also trusted, and the host allow-list is ignored. This is what lets a tag manager or bundler chain-load scripts without listing every CDN. It is the pattern Google recommends as “strict CSP” (web.dev).
Common gotcha: keeping 'unsafe-inline' next to a nonce is fine — CSP Level 2+ browsers ignore 'unsafe-inline' the moment a nonce or hash is present, and very old browsers fall back to it. The checker reports this as an INFO “present but ignored”, not as a weakness. What is not fine is a nonce that doesn't change per response: a static nonce is just 'unsafe-inline' with extra steps.

For the site owner and for the developer

For the site owner (plain English)

If a scanner, a pentest report or our checker says “Weak CSP: 'unsafe-inline' in script-src”, it means your site's script protection is switched off by one word in a header. Nothing is broken today, but if any form or page on the site has an injection bug, the header won't stop it. What to tell whoever runs the site: “move our inline scripts to nonces (or hashes) and remove 'unsafe-inline' from script-src; deploy it in Report-Only first.” Expect it to take a developer an afternoon on a typical site, longer if there are many third-party widgets.

Paste your current policy (or enter your domain) and see whether 'unsafe-inline' is actually weakening it, or is already ignored because a nonce is in place.

Check your CSP

For the developer (exact syntax)

Target policy — nonce plus strict-dynamic, with the legacy fallbacks that old browsers need:

Content-Security-Policy:
  script-src 'nonce-{RANDOM}' 'strict-dynamic' 'unsafe-inline' https:;
  object-src 'none';
  base-uri 'self';

Read it right to left: CSP3 browsers use the nonce and 'strict-dynamic' and ignore both 'unsafe-inline' and https:; CSP1 browsers ignore the nonce and fall back to the last two. Generate {RANDOM} fresh per response (16+ bytes, base64).

// Next.js (App Router) — middleware.ts
import { NextResponse } from "next/server";

export function middleware(request: Request) {
  const nonce = Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("base64");
  const csp = [
    `script-src 'nonce-${nonce}' 'strict-dynamic' 'unsafe-inline' https:`,
    "object-src 'none'",
    "base-uri 'self'",
  ].join("; ");
  const headers = new Headers(request.headers);
  headers.set("x-nonce", nonce);            // read it in layout.tsx via headers()
  const res = NextResponse.next({ request: { headers } });
  res.headers.set("Content-Security-Policy", csp);
  return res;
}
# nginx — a nonce needs application help; nginx alone can only set hashes or a static policy.
# Prefer computing the header in the app. If you must stay in nginx, use hashes:
add_header Content-Security-Policy "script-src 'self' 'sha256-<hash-of-inline-script>'; object-src 'none'; base-uri 'self'" always;
# Compute a hash for a static inline script (the exact bytes between the tags):
echo -n "window.dataLayer = window.dataLayer || [];" | openssl dgst -sha256 -binary | openssl base64

Inline event handlers (onclick="…") and javascript: URLs cannot be nonced — move them into a script file or a nonced block and attach listeners with addEventListener. Third-party snippets (analytics, chat widgets, tag managers) usually ship a nonce-aware install variant; search their docs for “CSP nonce”.

How do I remove it without breaking the site?

  • Add the new policy as Content-Security-Policy-Report-Only alongside the old one and point report-to at an endpoint — see the Report-Only rollout guide.
  • Fix every violation the reports show (each one is an inline script you haven't nonced yet).
  • When reports go quiet for a week, rename the header to the enforcing Content-Security-Policy.
  • Re-run the checker: the finding should change from MEDIUM to the INFO “present but ignored” (if you kept the fallback) or disappear.

Frequently asked questions

Is 'unsafe-inline' in Content-Security-Policy a vulnerability?

Not by itself — it is a weakness in a defence, not a hole an attacker can use directly. A Content-Security-Policy exists mainly to stop cross-site scripting payloads from executing, and those payloads arrive as inline script. Allowing 'unsafe-inline' in script-src tells the browser to run inline script anyway, so the policy no longer protects you from the very bug it was meant to contain. If the rest of your site has no injection flaw, nothing happens; if it does, the header won't help. That is why security scanners flag it as a medium-severity misconfiguration rather than a critical issue: fix it deliberately, with nonces or hashes, but don't panic.

Does 'unsafe-inline' with a nonce still weaken the policy?

No. The CSP specification says that when a script-src directive contains a nonce or a hash, browsers that understand CSP Level 2 or later must ignore 'unsafe-inline' entirely. Keeping it in the policy is a deliberate backwards-compatibility pattern: very old browsers that don't understand nonces fall back to 'unsafe-inline' and the page keeps working, while every modern browser enforces the nonce. Our checker recognises this and reports 'present but ignored' as an informational note instead of a weakness. The one thing that does weaken it is a nonce that never changes between responses — a static nonce is guessable, which makes it equivalent to 'unsafe-inline'.

What is the difference between 'unsafe-inline' and 'unsafe-eval'?

'unsafe-inline' controls whether the browser may run script that is written directly into the HTML — inline script blocks, onclick attributes and javascript: URLs. 'unsafe-eval' controls whether already-running JavaScript may turn strings into code with eval(), new Function(), or string arguments to setTimeout. Both are escape hatches that weaken CSP, but they cover different attack paths: injected HTML needs 'unsafe-inline', while an attacker who can already influence a string that gets eval'd needs 'unsafe-eval'. Most sites can drop 'unsafe-eval' by upgrading or replacing one legacy library; dropping 'unsafe-inline' usually needs nonces or hashes because inline scripts are far more common.

How do I find which inline scripts need a nonce?

Deploy the strict policy in Report-Only mode with a report-to endpoint. The browser will send one violation report for each inline script it would have blocked, including the page URL, a sample of the script and, in most browsers, the line number. Work through the list: move page code into a nonced block, replace onclick-style attributes with addEventListener calls in a script file, and switch third-party snippets to their nonce-aware install variant. When the reports stop arriving for a few days of normal traffic, rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy. Our CSP Checker will then show the finding gone or downgraded to the informational 'ignored' note.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →