# script-src-elem vs script-src-attr, which CSP directive controls what (/en/blog/script-src-elem-vs-script-src-attr)





[`script-src-elem`](/en/docs/web-security/policies/content-security-policy/directives/script-src-elem)
governs `<script>` elements, external and inline. [`script-src-attr`](/en/docs/web-security/policies/content-security-policy/directives/script-src-attr)
governs inline event handler attributes such as `onclick`. Both are finer-grained
splits of [`script-src`](/en/docs/web-security/policies/content-security-policy/directives/script-src),
and both fall back to it. Most sites never set them separately, but knowing the
split explains a lot of confusing Content Security Policy (CSP) violation reports.

## The fallback chain [#the-fallback-chain]

CSP resolves script controls from specific to general. When the browser needs to
decide whether a `<script>` element may run, it looks for `script-src-elem`. When
it decides whether an inline event handler may run, it looks for `script-src-attr`.
If the specific directive is absent, the check falls back, first to `script-src`,
then to [`default-src`](/en/docs/web-security/policies/content-security-policy/directives/default-src),
following the fallback list defined in CSP Level 3.

```text
script-src-elem  ->  script-src  ->  default-src
script-src-attr  ->  script-src  ->  default-src
```

So a policy with only `script-src` still controls both elements and inline
handlers, because both fall back to it. You reach for the granular directives when
you want to treat the two cases differently.

One rule matters more than the diagram suggests: fallback is replacement, not
merging. If your policy contains `script-src-elem`, the browser uses it alone for
element checks and ignores `script-src` entirely for that decision. Sources you
listed in `script-src` do not carry over. A policy like
`script-src 'self' cdn.example; script-src-elem 'nonce-abc'` will block a script
from `cdn.example` unless it carries the nonce, because the moment
`script-src-elem` exists, it is the whole rulebook for elements.

## script-src-elem vs script-src-attr [#script-src-elem-vs-script-src-attr]

|                      | `script-src-elem`                         | `script-src-attr`                                |
| -------------------- | ----------------------------------------- | ------------------------------------------------ |
| Controls             | `<script>` elements (external and inline) | inline event handler attributes (`onclick`, ...) |
| Falls back to        | `script-src`, then `default-src`          | `script-src`, then `default-src`                 |
| Nonces               | Apply                                     | Do not apply (no attribute to carry one)         |
| Hashes               | Apply to inline `<script>` content        | Need `'unsafe-hashes'` to match a handler        |
| `javascript:` URLs   | Governed here (per CSP Level 3)           | Not governed here                                |
| Typical strict value | `'nonce-...' 'strict-dynamic'`            | `'none'`                                         |

Browser support is no longer a reason to avoid them. Blink shipped both
directives years before the others; WebKit followed, and Gecko was the last
engine to add them. All three now support `script-src-elem`, `script-src-attr`,
and `'unsafe-hashes'`, and older browsers that do not recognize the granular
directives simply keep using your `script-src`, so the fallback doubles as a
compatibility net.

## script-src-elem, the elements directive [#script-src-elem-the-elements-directive]

`script-src-elem` decides which `<script>` elements the browser will execute. That
covers both external scripts (`<script src="...">`) and inline `<script>` blocks.

This is where [nonces and hashes](/en/docs/web-security/policies/content-security-policy/values/csp-hashes-nonce)
apply. A `'nonce-...'` source matches a `<script>` that carries the same `nonce`
attribute, and a `'sha256-...'` hash matches an inline `<script>` whose exact
content hashes to that value. `'strict-dynamic'` also lives here: it lets a
nonce-trusted script load further scripts, as covered in
[what strict-dynamic does](/en/blog/strict-dynamic-csp).

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

`script-src-elem` takes the same source expressions as `script-src`. One detail
that surprises people is `javascript:` URLs. CSP Level 3 checks a `javascript:`
navigation against `script-src-elem`, not `script-src-attr`, even though it looks
attribute-shaped when it sits in an `href`. That check is also the one place
[`'unsafe-hashes'`](/en/docs/web-security/policies/content-security-policy/values/csp-keywords)
matters on the element side: a hash can only match a
`javascript:` URL when that keyword is present.

## script-src-attr, the inline handler directive [#script-src-attr-the-inline-handler-directive]

`script-src-attr` decides whether inline event handler attributes may run:
`onclick`, `onerror`, `onload`, and the rest. It does not touch `<script>`
elements at all.

Nonces do not apply to an attribute (there is nowhere to put the `nonce`), so the
only way to allow a handler is `'unsafe-inline'`, or a hash of the handler's code
combined with `'unsafe-hashes'`. The hash alone is not enough: matching a
hash against an event handler or a `style` attribute requires the extra
`'unsafe-hashes'` keyword, which re-opens the inline-handler surface. That is why
the cleaner fix is to remove inline handlers and wire them with `addEventListener`,
as covered in [why you should never use unsafe-inline](/en/blog/unsafe-inline-csp).

There is a second trap here. `'unsafe-inline'` is ignored whenever a nonce or hash
appears in the same source list. So `script-src 'nonce-abc' 'unsafe-inline'` does
not quietly allow your `onclick` handlers; the nonce disables the
`'unsafe-inline'` keyword, and the handlers stay blocked. If you genuinely need to
allow handlers next to a nonced policy, the allowance has to live in its own
`script-src-attr` directive.

```http
Content-Security-Policy: script-src-attr 'none'
```

## A worked example [#a-worked-example]

Take a page with three things: an external script, an inline `<script>` block, and
a button with an inline `onclick` handler.

```html
<script src="/app.js" nonce="r4nd0m"></script>
<script nonce="r4nd0m">init();</script>
<button onclick="save()">Save</button>
```

Under a single directive, everything is judged by `script-src`:

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

The external and inline `<script>` elements run because they carry the nonce. The
`onclick` handler is blocked, because a nonce cannot match an attribute. The
violation report names `script-src-attr` as the effective directive, even though
you only wrote `script-src`, because that is the specific directive that governs an
inline handler.

Now split the two:

```http
Content-Security-Policy:
    script-src-elem 'nonce-r4nd0m' 'strict-dynamic';
    script-src-attr 'none'
```

Same outcome, but now each surface reports against its own directive. A blocked
`<script>` produces a violation with `script-src-elem` as the effective directive;
a blocked handler produces one with `script-src-attr`. Splitting makes the reports
unambiguous about which surface failed, which is useful when you are tightening a
policy and want to see inline handlers separately from script elements.

## Reading the violation reports [#reading-the-violation-reports]

The granular directive is what shows up in your telemetry, whether you set it or
not. CSP Level 3 requires the browser to report the effective directive for the
check, so an inline handler violation always says `script-src-attr`. In a
Reporting API [csp-violation report](/en/docs/web-security/reporting-api/reports/csp-violation)
the field is `effectiveDirective`; the legacy `report-uri`
JSON uses `effective-directive`, plus `violated-directive` as a historical alias.
The field-by-field breakdown is in
[what every CSP violation report field means](/en/blog/csp-violation-report-fields).

Chromium spells the fallback out in the console too. A blocked handler under a
plain `script-src` policy logs a message ending with "Note that 'script-src-attr'
was not explicitly set, so 'script-src' is used as a fallback." When you see
`script-src-attr` in a report, read it as "an inline event handler attribute was
blocked," then decide whether to fix the markup or knowingly allow it.

That distinction is the practical reason to collect reports rather than read the
console. CentralCSP groups incoming csp-violation reports by `effectiveDirective`, so
`script-src-elem` and `script-src-attr` land in separate rows even when your policy
only sets `script-src`. Blocked elements and blocked inline handlers need different
fixes, and separating them is what tells you whether you have a third-party script
problem or a markup problem.

<img alt="The directive column showing script-src-elem and script-src-attr as separate rows" src="__img0" width="1359" height="248" />

## Recommendation [#recommendation]

Keep it simple. Ship a strict `script-src` built on a nonce plus `'strict-dynamic'`,
and add `script-src-attr 'none'` to kill inline event handlers outright. That one
extra directive removes an entire injection surface and does not affect your
`<script>` elements, which the nonce already governs. Reach for `script-src-elem`
only when you genuinely need element scripts and inline handlers on different
rules; for most sites the policy below, the OWASP strict CSP plus
`script-src-attr 'none'`, is enough. Check the finished policy with the
[CSP evaluator](/tools/csp-evaluator).

```http
Content-Security-Policy:
    script-src 'nonce-r4nd0m' 'strict-dynamic';
    script-src-attr 'none';
    object-src 'none';
    base-uri 'none'
```

The same split exists for styles:
[`style-src-elem`](/en/docs/web-security/policies/content-security-policy/directives/style-src-elem)
covers `<style>` elements and stylesheet links,
[`style-src-attr`](/en/docs/web-security/policies/content-security-policy/directives/style-src-attr)
covers inline `style` attributes, and both fall back to `style-src` the same way.

## FAQ [#faq]

### Why does my CSP report say script-src-attr when I never set it? [#why-does-my-csp-report-say-script-src-attr-when-i-never-set-it]

Because the browser reports the directive that governs the check, not the one you
wrote. `effectiveDirective: script-src-attr` means an inline event handler
attribute (`onclick`, `onerror`, ...) was blocked; your `script-src` was only
consulted as the fallback. Fix the handler by moving it to `addEventListener`, or
knowingly allow it via `script-src-attr`. Browser extensions that inject handlers
are another common source of this noise.

### Why is my nonce not working for onclick handlers? [#why-is-my-nonce-not-working-for-onclick-handlers]

Nonces can never allow an inline event handler. The spec only consults nonces for
`<script>` and `<style>` elements, and an attribute has nowhere to carry a nonce.
Adding `'unsafe-inline'` next to the nonce does not help either, because a nonce
or hash in the source list causes `'unsafe-inline'` to be ignored. Rewrite the
handler with `addEventListener`, or allow the exact code with
`script-src-attr 'unsafe-hashes' 'sha256-...'`.

### What is the difference between script-src and script-src-elem? [#what-is-the-difference-between-script-src-and-script-src-elem]

`script-src-elem` is the element-only subset: it governs `<script>` elements and
nothing else, while `script-src` also covers inline event handlers and `eval`-style
execution. The [script-src-elem reference](/en/docs/web-security/policies/content-security-policy/directives/script-src-elem)
has the full breakdown.

### Do I need to set script-src-elem? [#do-i-need-to-set-script-src-elem]

No. When it is absent, `script-src` covers script elements, and `default-src`
covers them if `script-src` is absent too. Set it only when elements and handlers
need different rules, and remember that setting it replaces `script-src` for
element checks rather than merging with it, so it must list every source your
scripts need.

### What does unsafe-hashes do in CSP? [#what-does-unsafe-hashes-do-in-csp]

`'unsafe-hashes'` is a CSP Level 3 keyword that lets hash sources match inline
event handlers, inline `style` attributes, and `javascript:` navigations, which
plain hashes never match. It is safer than `'unsafe-inline'` because only the
exact hashed code can run, but weaker than removing handlers, since the hashed
code is then allowed in any handler on the page.

## Related reading [#related-reading]

* [script-src-elem](/en/docs/web-security/policies/content-security-policy/directives/script-src-elem)
  and [script-src-attr](/en/docs/web-security/policies/content-security-policy/directives/script-src-attr),
  the directive references.
* [Why you should never use unsafe-inline in CSP](/en/blog/unsafe-inline-csp),
  for removing inline handlers and the `'unsafe-hashes'` trade-off.
* [What strict-dynamic does and when to use it](/en/blog/strict-dynamic-csp)
* [What every CSP violation report field means](/en/blog/csp-violation-report-fields)

## Sources [#sources]

* [W3C, Content Security Policy Level 3](https://www.w3.org/TR/CSP3/) (directive
  fallback list, effective directive for inline checks, violation reporting)
* [W3C, CSP editor's draft](https://w3c.github.io/webappsec-csp/) (`'unsafe-hashes'`
  definition)
* [Chromium Blink, csp\_directive\_list.cc](https://github.com/chromium/chromium/blob/main/third_party/blink/renderer/core/frame/csp/csp_directive_list.cc)
  (inline type mapping, `'unsafe-hashes'` gating, fallback console message)
* [Chromium, csp\_source\_list.cc](https://github.com/chromium/chromium/blob/main/services/network/public/cpp/content_security_policy/csp_source_list.cc)
  (`'unsafe-inline'` suppressed by nonces and hashes)
* [MDN, script-src-elem](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src-elem)
* [MDN, script-src-attr](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src-attr)
* [MDN, script-src](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy/script-src)
* [MDN, CSPViolationReportBody](https://developer.mozilla.org/en-US/docs/Web/API/CSPViolationReportBody)
* [MDN browser-compat-data, Content-Security-Policy](https://github.com/mdn/browser-compat-data/blob/main/http/headers/Content-Security-Policy.json)
* [OWASP, Content Security Policy cheat sheet](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html)
