Why does unsafe-inline defeat a Content-Security-Policy?

A Content-Security-Policy that includes the unsafe-inline value tells the browser to allow inline script and style, which is exactly the content an attacker injects when they can write into your HTML. MDN warns that allowing unsafe-inline defeats much of the purpose of a Content-Security-Policy, because the policy can no longer separate your own inline scripts from an attacker's. The unsafe-inline value never looks broken, because pages keep working, so the weakness usually ships unnoticed.

Why this matters

The script-src directive lists the sources from which a document may execute script. When unsafe-inline appears in that list, the browser treats any inline script element and any inline event handler as trusted, regardless of where the surrounding document came from. An attacker who can inject markup, for example through an unescaped comment field or a template that concatenates user input, then runs their own code in your origin with the document's full privileges. The policy header is present, so a scanner sees a policy, but the protection has effectively been switched off by a single token. Because unsafe-inline is often added to silence a console error during a migration and then forgotten, it is one of the most common ways a real policy quietly becomes decorative.

How to fix it

The Content Security Policy Level 3 specification defines the nonce source, written as nonce- followed by a base64 value, and it defines hash sources computed over the script contents. A nonce is unique per HTTP request: the server generates a fresh random value, places the same value in the response header and in the nonce attribute of each legitimate script element, and the browser executes only scripts whose nonce matches. A hash works for static inline code, where the header carries the base64 digest of the exact script body. MDN notes that if a directive contains both a nonce and the unsafe-inline keyword, the browser ignores unsafe-inline, so a correct nonce takes precedence. The strict-dynamic keyword lets a trusted script load further scripts, which removes the need to list every CDN origin.

Content-Security-Policy: script-src 'nonce-r4nd0mPerRequest' 'strict-dynamic'

<script nonce="r4nd0mPerRequest" src="https://cdn.example.com/app.js"></script>

# Compute a hash for a static inline script
cat app.js | openssl dgst -sha256 -binary | openssl base64 -A

How to verify the fix

Deploy the policy in report-only mode first, so the browser reports what a strict policy would break without blocking anything, then switch to enforcing once the reports are clean across a full request cycle, including logged-in flows, because a nonce that a CDN caches is reused across users and quietly weakens the protection. If a page depends on a third-party inline snippet you do not control, vendor the script or add its hash rather than reintroducing unsafe-inline.

Content-Security-Policy-Report-Only: script-src 'self' 'nonce-r4nd0mPerRequest' 'strict-dynamic'

Automated check

The Auditor fetches your page and, when it finds inline scripts without a nonce, raises a Multiple inline scripts without nonce finding at Low severity. That surfaces the pattern to fix, but it does not design your policy, so pair the finding with strict-dynamic and remove unsafe-inline yourself.

Scan your app free →

Related issues