'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 asnonce="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).
'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 base64Inline 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-Onlyalongside the old one and pointreport-toat 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.