← All Tools

Why 'unsafe-inline' Defeats CSP — and the Nonce/Hash Alternative

Guide · Last verified Aug 28, 2026

One of the most common mistakes when first rolling out a Content-Security-Policy (CSP) header is habitually tacking 'unsafe-inline' onto something like script-src 'self' 'unsafe-inline'. It's tempting to move on once the page works with no console errors, but this single keyword erases the very reason CSP exists in the first place. This guide walks through why, how the CSP Generator actually handles this issue, and the spec-level limits of the meta-tag approach.

1. What CSP is actually defending against

CSP's core purpose is defending against XSS (cross-site scripting). Say an attacker injects code like <script>alert(document.cookie)</script> through a comment field or URL parameter — without CSP, that script runs as-is and could leak session cookies. CSP is a header that whitelists the origins a browser is allowed to execute scripts from. Any script from an origin not listed in script-src is simply refused execution by the browser.

2. Why 'unsafe-inline' switches this defense off entirely

The catch is that CSP, by default, blocks all inline scripts — <script> tags written directly in the HTML, event attributes like onclick — regardless of origin. Adding the 'unsafe-inline' keyword to script-src lifts that inline-blocking rule entirely. The problem is that most attacker-injected scripts are inline too, precisely because it's easier: no need to host a new file anywhere, just inject text. So the moment you add 'unsafe-inline', you disable the very inline-blocking mechanism that was CSP's reason for existing — "restrict origins to a whitelist" — and the browser can no longer distinguish a legitimately loaded script from an attacker-injected one. No matter how tightly you configure other directives (img-src, font-src, etc.), leaving 'unsafe-inline' in script-src makes CSP functionally equivalent to not having XSS protection at all.

3. The nonce approach: "a password valid for this request only"

Banning inline scripts outright is often impractical in real applications (e.g. server-rendered pages that need to embed dynamic values immediately). That's where nonce (number used once) comes in. The server generates a random string on every request and places it in two spots simultaneously: the nonce="random-value" attribute on the script tag, and 'nonce-random-value' in the CSP header. The browser only allows execution of an inline script whose nonce attribute exactly matches. Even if an attacker injects a script into the page, they have no way to know the nonce issued for that specific request — it changes every time and is unpredictable unless the HTML response itself is intercepted — so the injected script is blocked due to a nonce mismatch.

4. The hash approach: "the content itself is the pass"

For static sites where the server can't inject a fresh random value per request, the hash approach works instead. You compute a hash (e.g. SHA-256) of the exact content of the inline script you want to allow, and register it in CSP as 'sha256-hash-value'. The browser computes the hash of any inline script it's about to run and compares it against the registered value. Change even a single character and the hash changes, so injected code can never match. The tradeoff is that any time the script's content changes, you must recompute the hash and update the CSP — making this best suited to static inline code that rarely changes.

Comparison example: here's what happens to the same inline script under three different CSP configurations.
CSP script-src settingLegitimate inline scriptAttacker-injected script
'unsafe-inline'ExecutesExecutes (not blocked)
'nonce-a1b2c3...'Executes if nonce matchesBlocked — no nonce
'sha256-Xy8f...'Executes if hash matchesBlocked — hash mismatch

5. Spec-level limits when using CSP via a meta tag

Many people try to apply CSP purely with <meta http-equiv="Content-Security-Policy" content="...">, without touching server config — but there are hard spec limits here. frame-ancestors, report-uri, report-to, and sandbox simply don't work in a meta tag — they're only honored when delivered as an HTTP response header. frame-ancestors in particular is used for clickjacking protection, which means you can never turn that defense on via a meta tag, period. If you check the CSP Generator's code, you'll find these four directives explicitly listed in a META_UNSUPPORTED array, and the tool automatically excludes them when generating output meant for an HTML meta tag. In other words, the tool already accounts for this constraint at the code level, so users don't get confused wondering why frame-ancestors "isn't working" after adding it to the meta tag output. In production, delivering CSP as an HTTP header directly from the server (Nginx/Apache config or application code) is recommended over the meta-tag approach.

6. Configuration isn't done until it's validated

CSP has syntax quirks where the browser silently ignores a malformed part rather than raising an error, so it's easy to miss a broken directive just by eyeballing it. After generating your header value with the CSP Generator, it's safer to run it back through the CSP Validator to confirm the syntax is valid and no risky leftovers remain (like a stray '*' or 'unsafe-inline' in script-src).

Frequently Asked Questions

Q. Does adding 'unsafe-inline' make CSP completely pointless?

A. Not entirely. Other directives like img-src and connect-src still restrict resource origins. But 'unsafe-inline' on script-src defeats CSP's core purpose of blocking XSS, so from a security standpoint that specific defense is effectively off.

Q. Which is safer, nonce or hash?

A. Roughly equivalent security, but different use cases. Nonce is easier to manage when the server can generate a fresh value per request. Hash works without any server logic for fixed, static inline scripts.

Q. Does setting frame-ancestors in a meta-tag CSP throw an error?

A. No error is shown — the browser just silently ignores that directive. This is a classic trap where you think it's configured but it isn't. If you need clickjacking protection, deliver CSP via an HTTP response header instead.

Q. Can I use external scripts like Google Analytics without 'unsafe-inline'?

A. Yes. Whitelist the external domain (e.g. googletagmanager.com) in script-src, and allow only your page's inline initialization code separately via nonce or hash — no 'unsafe-inline' required.