All posts

script-src-elem vs script-src-attr, which CSP directive controls what

CentralCSP Team ·

Last update:

script-src-elem governs <script> elements, external and inline. script-src-attr governs inline event handler attributes such as onclick. Both are finer-grained splits of 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

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, following the fallback list defined in CSP Level 3.

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-elemscript-src-attr
Controls<script> elements (external and inline)inline event handler attributes (onclick, ...)
Falls back toscript-src, then default-srcscript-src, then default-src
NoncesApplyDo not apply (no attribute to carry one)
HashesApply to inline <script> contentNeed 'unsafe-hashes' to match a handler
javascript: URLsGoverned 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 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 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.

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' 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 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.

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.

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

A worked example

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

<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:

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:

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

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 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.

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.

The directive column showing script-src-elem and script-src-attr as separate rows

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.

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 covers <style> elements and stylesheet links, style-src-attr covers inline style attributes, and both fall back to style-src the same way.

FAQ

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?

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?

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 has the full breakdown.

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?

'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.

Sources