# Why you should never use unsafe-inline in CSP (/en/blog/unsafe-inline-csp)





If your Content Security Policy (CSP) contains `'unsafe-inline'`, it is mostly decorative. CSP is an HTTP response header that tells the browser which scripts and styles it is allowed to run. Its single most useful job is blocking inline JavaScript, which is the exact thing most cross-site scripting (XSS) attacks rely on. The `'unsafe-inline'` keyword turns that protection back off.

The short version: do not ship `'unsafe-inline'` in a script directive. Move inline scripts into external files, or allow the ones you keep with a nonce or a hash. The rest of this post explains what the keyword actually does, why it defeats CSP, and how to migrate off it safely using Report-Only first.

New to CSP? [Get started with Content Security Policy](/en/blog/get-started-with-csp) covers the basics first, what CSP is, your first policy, and your first nonce.

## What does 'unsafe-inline' do? [#what-does-unsafe-inline-do]

By default, a CSP that sets `script-src` blocks all inline JavaScript. Adding `'unsafe-inline'` to that directive re-enables it. For scripts, it allows three things the browser would otherwise refuse to run:

* Inline `<script>` blocks (code written directly in the page).
* Inline event handler attributes such as `onclick`, `onerror`, and `onload`.
* `javascript:` URLs.

There is a style equivalent. In `style-src`, `'unsafe-inline'` allows inline `<style>` blocks and `style="..."` attributes. The styling case is lower risk than the scripting case, but the same migration ideas apply.

So the keyword does exactly what it says: it tells the browser inline code is safe to run. The problem is that the browser has no way to tell your inline code from an attacker's.

## Why it defeats CSP's XSS protection [#why-it-defeats-csps-xss-protection]

XSS works by getting the browser to execute markup the attacker controls. A classic injection looks like an inline `<script>` block, an `<img onerror=...>` handler, or a `javascript:` URL slipped into a page through unsanitized input.

With `'unsafe-inline'` present, the browser runs any inline script regardless of where it came from. Injected inline script executes exactly like your legitimate inline script, and CSP raises no barrier. The directive that was supposed to be your second line of defense against XSS is now waving the attack through.

Nonces and hashes close this gap because they require something the attacker cannot supply. A nonce is a fresh random value the server generates per response and the attacker cannot guess. A hash matches one specific, known block of code, and arbitrary injected code will not match it. Either way, the browser runs your inline script and refuses the injected one.

CSP is defense in depth, not a substitute for input sanitization. You still escape and validate input. CSP is the layer that contains the damage when something slips through, and `'unsafe-inline'` removes that layer. To go further and [enable Trusted Types to kill DOM XSS](/en/blog/enable-trusted-types), you can stop injection at the sink as well.

## The safe alternatives [#the-safe-alternatives]

You have three ways to keep inline behavior working without `'unsafe-inline'`. Pick per case; most sites use a mix.

### 1. Externalize the code [#1-externalize-the-code]

Move inline scripts and styles into files served from an origin your policy already allows. Once the code lives in `/main.js` instead of a `<script>` block, there is no inline keyword to add at all.

```http
Content-Security-Policy: script-src 'self'
```

This is the cleanest option when you control the code. The trade-off is that small per-page snippets and inline config become extra files or are injected another way.

### 2. Use a nonce [#2-use-a-nonce]

A nonce is a cryptographically random value you regenerate on every HTTP response. You put it in the header and repeat it on each inline element via the `nonce` attribute. The browser runs only the inline scripts whose `nonce` matches the header. For the per-framework details, see [how to set up a nonce](/en/blog/csp-nonce-setup).

```javascript
const nonce = crypto.randomUUID();
res.setHeader(
  "Content-Security-Policy",
  `script-src 'nonce-${nonce}'`
);
// <script nonce="${nonce}" src="/main.js"></script>
// <script nonce="${nonce}">console.log("hello!");</script>
```

The nonce must be unpredictable and unique per response. Generating it once and reusing it across requests breaks the whole point, because an attacker could read it from one page and reuse it. A common recommendation is at least 128 bits of entropy.

### 3. Use a hash [#3-use-a-hash]

A hash allowlists one exact block of inline code by its content. You compute a SHA-256, 384, or 512 digest of the inline content, base64-encode it, and add it as a source like `'sha256-...'`.

```http
Content-Security-Policy: script-src 'sha256-...'
```

The hash is computed over the exact inline content, the text between the tags, not the tags themselves. Whitespace and capitalization are part of the content, so a single changed space invalidates the hash. Paste the snippet into our [hash generator](/tools/csp-hash) and it returns the `'sha256-...'` source value to drop straight into your policy, or follow the walkthrough on [how to generate a CSP hash](/en/blog/csp-hash-sha256).

Hashes suit static inline blocks that rarely change, such as a build-time inline bundle. Nonces suit server-rendered pages where you control the response.

### The inline event handler gotcha [#the-inline-event-handler-gotcha]

Nonces and hashes do not cover inline event handlers like `onclick`. Allowing a handler by hash requires the separate `'unsafe-hashes'` keyword, which loosens the policy. The better fix is to remove inline handlers entirely and wire them up with `addEventListener` in your external or nonced script.

```html
<!-- Before: inline handler, needs 'unsafe-inline' or 'unsafe-hashes' -->
<button onclick="saveForm()">Save</button>
```

```javascript
// After: no inline handler, works under a strict nonce/hash policy
// markup: <button id="save">Save</button>
document.getElementById("save").addEventListener("click", saveForm);
```

## How the directives fit together [#how-the-directives-fit-together]

When you go deeper, CSP splits inline handling across more specific directives, and [script-src-elem vs script-src-attr](/en/blog/script-src-elem-vs-script-src-attr) compares the two that matter for inline code:

* `script-src` governs inline script when the more specific directives are absent.
* `script-src-elem` governs inline `<script>` blocks.
* `script-src-attr` governs inline event handlers. Its only meaningful values are `'unsafe-hashes'`, `'unsafe-inline'`, and `'report-sample'`.

Styles mirror this with `style-src`, `style-src-elem`, and `style-src-attr`. The `-elem` and `-attr` directives fall back to `script-src` or `style-src`, which fall back to `default-src`.

`'unsafe-inline'` has a sibling weakening keyword worth knowing, [`'unsafe-eval'`](/en/blog/unsafe-eval-csp), which re-enables string-to-code execution the same way this one re-enables inline script.

One rule does a lot of work here. If a directive contains a nonce-source or a hash-source, the browser ignores `'unsafe-inline'` in that same directive, defined by the W3C allow-all-inline algorithm and matched in Chromium's `AllowAllInline()`. So if `'unsafe-inline'` ever ends up next to a nonce or hash, the nonce or hash still protects you. Do not rely on that as a feature, just remove `'unsafe-inline'`.

For script directives, `'strict-dynamic'` goes further. It tells the browser to ignore host allowlists, scheme sources, `'self'`, and `'unsafe-inline'` entirely, and to trust scripts only through a nonce or hash on a root script (and the scripts that root loads). It has no effect on `style-src`, and it is the modern way to allowlist scripts without fragile host lists:

```http
Content-Security-Policy: script-src 'nonce-r4nd0m' 'strict-dynamic'
```

`'unsafe-inline'`, `script-src`, `style-src`, nonces, hashes, and `'strict-dynamic'` are all widely supported in current browsers.

<img alt="Violation rows whose blocked origin reads inline rather than a host" src="__img0" width="1359" height="248" />

## A safe migration path off 'unsafe-inline' [#a-safe-migration-path-off-unsafe-inline]

Removing `'unsafe-inline'` from a live site will break anything that depends on it, so move in stages and watch reports before you enforce. This is the same Report-Only-first approach covered in [how to build a strong CSP, step by step](/en/blog/how-to-build-a-strong-csp).

1. **Audit your inline usage.** Find every inline `<script>`, every inline event handler, and every `javascript:` URL. Our [CSP scanner](/tools/csp-scanner) and [CSP evaluator](/tools/csp-evaluator) flag where a policy still leans on `'unsafe-inline'`.
2. **Convert each case.** Externalize what you can, add a nonce to server-rendered inline scripts, hash the static ones, and replace inline handlers with `addEventListener`.
3. **Test in Report-Only.** Ship the new policy on the `Content-Security-Policy-Report-Only` header. The browser does not block anything, it only sends a violation report for what the policy would have blocked. Collect those reports and fix the gaps.
4. **Enforce.** When Report-Only is quiet, move the policy to the enforcing `Content-Security-Policy` header and drop `'unsafe-inline'` from your script directives.

Report-Only is the step that makes this safe. You see every violation a stricter policy would cause, on real traffic, before any user hits a broken page. To inspect a single page on your own machine, you can also [debug CSP violations in DevTools](/en/blog/debug-csp-violations-devtools).

CentralCSP collects those Report-Only violation reports for you, groups them, and shows the inline scripts running on each page through CSP hash reporting, so you can see exactly what to externalize, nonce, or hash before you flip to enforce. That is the core of our [CSP suite](/platform/csp-builder). You can [start a free trial](/register) and point a Report-Only header at it to watch the migration in real time.

For the full directive and keyword reference, see the [CSP policy reference](/en/docs/web-security/policies/content-security-policy). For more on the keyword that lets trusted scripts load further scripts, see [the `'strict-dynamic'` guide](/en/blog/strict-dynamic-csp). For a worked example of nonce plus `'strict-dynamic'` with a real third party, see [running Google Analytics and Tag Manager under a strict CSP](/en/blog/csp-google-analytics-tag-manager).

## Frequently asked questions [#frequently-asked-questions]

### Is 'unsafe-inline' safe? [#is-unsafe-inline-safe]

No. It re-enables exactly the inline script execution CSP exists to block, so an injected inline script runs like any other. Keep it only as a fallback in a directive that also carries a nonce or hash, where modern browsers ignore it.

### What is the difference between a nonce and a hash? [#what-is-the-difference-between-a-nonce-and-a-hash]

A nonce is a fresh random value you place on the header and on each inline element per response, best for server-rendered pages. A hash allowlists one exact, unchanging block of inline code, best for static snippets.

### Does 'strict-dynamic' replace 'unsafe-inline'? [#does-strict-dynamic-replace-unsafe-inline]

For scripts, yes. With `'strict-dynamic'` the browser ignores `'unsafe-inline'`, host allowlists, and `'self'`, and trusts scripts only through a nonce or hash. It does not apply to styles.

### Is 'unsafe-inline' ever harmless? [#is-unsafe-inline-ever-harmless]

Only when the same directive also has a nonce or hash, because the browser then ignores `'unsafe-inline'`. Even then, remove it so the policy says exactly what it means.

## The takeaway [#the-takeaway]

`'unsafe-inline'` is not a convenience flag, it is an off switch for the protection you turned CSP on to get. Remove it from your script directives. If it ever sits beside a nonce or hash, the browser ignores it anyway, so you lose nothing by dropping it. Externalize, nonce, or hash your inline code, and migrate through Report-Only so you never break the page to secure it.

Further reading: the [MDN guide to CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP) and the [W3C CSP Level 3 specification](https://www.w3.org/TR/CSP3/).
