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

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.
Related reading
- script-src-elem and script-src-attr, the directive references.
- Why you should never use unsafe-inline in CSP,
for removing inline handlers and the
'unsafe-hashes'trade-off. - What strict-dynamic does and when to use it
- What every CSP violation report field means
Sources
- W3C, Content Security Policy Level 3 (directive fallback list, effective directive for inline checks, violation reporting)
- W3C, CSP editor's draft (
'unsafe-hashes'definition) - Chromium Blink, csp_directive_list.cc
(inline type mapping,
'unsafe-hashes'gating, fallback console message) - Chromium, csp_source_list.cc
(
'unsafe-inline'suppressed by nonces and hashes) - MDN, script-src-elem
- MDN, script-src-attr
- MDN, script-src
- MDN, CSPViolationReportBody
- MDN browser-compat-data, Content-Security-Policy
- OWASP, Content Security Policy cheat sheet