[5] CSP style-src-attr skipped for style attributes parsed in inert documents
CSP blocked the style attribute, unless it was parsed somewhere with no policy.
Low. This is a real CSP bypass reachable from web content, but it needs an existing HTML-injection sink that stages markup through an inert document, and the ceiling is CSS-level visual control: no script execution and no cross-origin access.
Content Security Policy is meant to hold regardless of the route markup took to reach a page's DOM. Documents with no browsing context, such as the results of DOMParser, createHTMLDocument() and <template> contents, parse markup without rendering it, and WebCore's StyledElement parses a style attribute into inline style only after asking the document's ContentSecurityPolicy. The expectation is that a page declaring style-src-attr 'none' never renders an author-supplied inline style, however the element got there.
The angle: an attacker who can inject HTML through a client-side sanitizer or template pipeline can apply inline CSS in a page whose policy forbids it, enabling overlays, hiding and UI spoofing.
CSP
style-src-attrdoes not block a style attribute parsed in a document with no browsing context.StyledElement::styleAttributeChanged()checked the attribute against the policy of the element's own document when set. A document created byDOMParser,createHTMLDocumentor a template starts with an empty policy, so the attribute was allowed and the parsed style stayed on the element. When the element moved into a page whose policy setsstyle-src-attrtonone, nothing checked it again. CSP Level 3 4.2.3 defines this check, and HTML 3.2.6.5 requires that blocked style rules are not applied. The fix checks against the policy of the document returned bycontextDocument(): the recorded creator for a DOMParser result, acreateHTMLDocumentdocument or template contents, and otherwise the element's own document. Three web-platform tests cover the DOMParser,createHTMLDocumentand template paths.
Source/WebCore/dom/StyledElement.cpp
LayoutTests/imported/w3c/web-platform-tests/content-security-policy/style-src/style-src-attr-blocked-domparser-adopt.html
Patch Details
In StyledElement.cpp, the allowInlineStyle call in styleAttributeChanged() moves into a new private helper, isStyleAttributeAllowedByContentSecurityPolicy(const AtomString&, OrdinalNumber), declared in StyledElement.h. The helper resolves Ref policyDocument = document().contextDocument() and queries that document's contentSecurityPolicy(), passing policyDocument->url() as the reporting URL. The other allowInlineStyle arguments are unchanged: CheckUnsafeHashes::Yes, the element, nonce() and isInUserAgentShadowTree(). The AttributeModificationReason::ByCloning short-circuit and the parser line-number lookup on the element's own document are also unchanged. Each of the three new tests parses <div style="background-color: green"> in an inert document under a page policy of style-src-attr 'none', moves the element into the live document, and asserts probe.style.length == 0 plus a securitypolicyviolation event with violatedDirective == 'style-src-attr'.
Security policy evaluated once at ingestion against an inert staging container's empty policy, with no re-check when content migrates into the rendering context.
Background
Content Security Policy.
CSP is a page-declared policy, sent as an HTTP header or <meta http-equiv>, restricting which resources and inline content the page may use. style-src-attr is the CSP Level 3 directive governing inline style="..." attributes, 'none' forbids all of them, and a blocked attribute is reported via a securitypolicyviolation event with blockedURI == 'inline'.
Inert documents.
A document without a browsing context is attached to no window. Examples are the result of DOMParser.parseFromString, document.implementation.createHTMLDocument(), and the content DocumentFragment of a <template> (owned by a separate template-contents document). They parse markup but run no scripts and render nothing. adoptNode and ordinary insertion (append) move a node, together with its already-parsed internal state, from one document into another.
Style attribute handling in WebCore.
StyledElement::attributeChanged() dispatches to styleAttributeChanged() whenever the style attribute value changes. That function parses the string into the element's m_inlineStyle (a StyleProperties held in ElementData) through setInlineStyleFromString(), gated on ContentSecurityPolicy::allowInlineStyle.
Document::contextDocument().
It returns the document treated as the logical owner for policy purposes: the creator document for documents lacking their own browsing context, or the document itself otherwise.
Analysis
This is a logic error that amounts to a policy bypass: the policy decision is bound to the wrong document. Before the fix, the CSP check for a style attribute ran once, inside styleAttributeChanged(), when the attribute was set, and it consulted this->document(). Inert documents receive no policy of their own (no response headers, and they are not the page that declared the <meta> policy), so allowInlineStyle passed and setInlineStyleFromString() parsed the declaration into the element's inline style. Nothing re-evaluates it later: adoptNode, append, or moving an element out of template.content carries the parsed style along with no CSP check on that path.
Page CSP: style-src-attr 'none'
Pre-fix: DOMParser doc ──► style attr set ──► check vs OWN doc policy ──► allowed
m_inlineStyle parsed ──► adoptNode/append into page ──► no re-check ──► style applied
Post-fix: DOMParser doc ──► style attr set ──► check vs contextDocument() (creator page)
──► blocked, violation reported, m_inlineStyle stays empty
The invariant that was missing: an inline style declaration is admitted only if it passes the policy of the context that will eventually render it. Inert documents are staging areas for content headed to their creator document, so their own policy is the wrong authority. The tests expect the violation event on window because the check now runs against the creator document's ContentSecurityPolicy object, so the report goes through that live document. The policyDocument->url() argument sets only the document URL recorded in the report; it does not choose where the event is dispatched. Under the old code the inert document's policy produced no violation at all. No memory is corrupted; the consequence is purely a policy-enforcement failure.
The bypass is reachable from web content, but only as a companion to an existing HTML-injection sink that stages markup through DOMParser, createHTMLDocument or <template>, a staging pattern common in client-side sanitizer and templating code. The primitive is arbitrary inline CSS on attacker-injected elements in a page whose CSP forbids style attributes. The ceiling is CSS-level visual control over those elements (layout, positioning, visibility, overlays). Fetches triggered by the CSS, such as background-image URLs, remain subject to directives like img-src. No process boundary is crossed.
The open questions are adjacent. Any other parse-time policy decision in WebCore that consults this->document() instead of contextDocument() has the same shape. If a staging path exists whose inert document records no creator, contextDocument() would fall back to the inert document's own policy and reintroduce the bypass. The retained AttributeModificationReason::ByCloning short-circuit is safe only if every cloned source was itself checked against the right policy, so a source parsed in an unrecorded-creator inert document and then cloned into the page deserves a test.
This weakens CSP's inline-style restriction, a defense-in-depth layer meant to contain HTML injection. The policy model assumes a page declaring style-src-attr 'none' never renders an author-supplied style attribute, whatever path the markup took. The plausible gain is visual: UI redressing, content spoofing, or overlaying and hiding page elements.
Insight: client-side sanitizers and templating libraries often parse untrusted markup in an inert document and then move the nodes into the live page, exactly the three paths this bug covers. CSP is the backstop for when such a sanitizer lets a style attribute through, and before the fix the backstop silently did not apply on those paths. Checks done eagerly at parse or attribute-set time bake the decision into internal state (m_inlineStyle), so the document consulted at that moment is security-critical. contextDocument() is WebKit's established answer to which document's policy governs an inert document.
Audit directions
- Policy checked against the staging container. A policy decision at ingestion time uses the staging container's policy instead of the eventual consumer's. The invariant is that a check whose result is cached into object state is evaluated against the context that will act on that state. Narrow: grep WebCore for
document().contentSecurityPolicy()anddocument->contentSecurityPolicy()call sites reachable from attribute-changed or parser-inserted handlers, and check whether each should usecontextDocument(). Good starting points are inline<style>element sheet creation,on*event-handler attribute compilation andjavascript:URL attributes. The match tell is a CSP query on the element's own document whose result is stored on the element (a parsed style, a compiled listener, a loaded sheet) with no recheck indidMoveToNewDocumentor on insertion. - Inert-document factories that may not record a creator. Narrow: enumerate document factories beyond the three tested (XHR
responseXML,createDocumentfor XML, XSLTProcessor output,Range.createContextualFragment,setHTMLUnsafe/parseHTMLUnsafe, declarative shadow DOM parsing) and confirm each records its context document; otherwise the fallback to the element's own document reintroduces the inert-policy check. Wider: the same question for any policy carried per document, such as Trusted Types enforcement, sandbox flags and referrer policy. Widest: any system that stages untrusted input in a less-privileged scratch container before promoting it, for example server-side templating that sanitizes in one context and renders in another, or a container image scanned under one policy and run under another. The tell is a promotion or move operation that transfers pre-validated state without re-running or inheriting the destination's policy. - Copy-trust fast paths. Verify the
AttributeModificationReason::ByCloningshort-circuit instyleAttributeChanged()cannot launder an unchecked style. Narrow: tracecloneNode/importNodefrom elements in inert documents into a CSP-restricted page and check whether the clone inheritsm_inlineStyleadmitted under a different policy. Wider: audit every "trusted because copied" bypass in WebCore attribute handling, since each one assumes the source was validated under the destination's policy. Widest: copy-trust assumptions appear wherever validation is skipped for duplicated objects, as in capability systems and copy-on-write ACL inheritance. The tell is a fast path that skips a policy check on the provenance of the operation, not on which policy the source was checked against.