← Chrome — 2026-W34

18edac1dcad4e478dfde86bf136370154d8efcfc  Consistently set XML parser 'already started' flag from parseFromString.

severity low class LogicError CVSS 4.3 signal score 3.8 third_party/blink/renderer/core/ AI review CONFIRMED
Structural signals: area:third_party/blink/renderer/core/ (+2.5), surgical (+1.3)

The bug at a glance

The prior fix used QualifiedName operator== (which compares prefix too) instead of Matches(), so prefixed script elements (e.g. xx:script in an SVG namespace) parsed by DOMParser did not get the already-started flag set and could therefore execute when adopted into a live document; it is a script-execution-defense bypass, but gated behind the DOMParserXmlScriptAlreadyStartedEnabled runtime feature.

An attacker who controls an XML/SVG string fed to DOMParser.parseFromString can smuggle a script that runs when the result is imported into a live document, bypassing the already-started mitigation simply by using a namespace prefix on the script element.

Root cause

When parsing via DOMParser, Blink sets CreateElementFlags::SetAlreadyStarted(true) on script elements so they will not execute later. The check compared q_name to html/svg script tags with operator==, which also matches the namespace prefix, so a script element with a non-empty prefix (e.g. xx:script) failed the comparison and was left with already-started unset. Switching to QualifiedName::Matches() compares namespace URI and local name while ignoring the prefix, so prefixed script elements are now correctly flagged as already-started and will not execute after import. This is a follow-on completing crrev.com/c/8215568.

Key code

Compare by Matches() so prefixed script tags are flagged already-started

-      (q_name == html_names::kScriptTag || q_name == svg_names::kScriptTag)) {
+      (q_name.Matches(html_names::kScriptTag) ||
+       q_name.Matches(svg_names::kScriptTag))) {
    flags.SetAlreadyStarted(true);

Patch walkthrough

  • third_party/blink/renderer/core/xml/parser/xml_document_parser.cc — Replaces QualifiedName operator== with Matches() so script elements carrying a namespace prefix are recognized and marked already-started, closing the script-execution bypass.
  • third_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc — Same Matches() fix applied in the Rust-backed XML parser path so both parser implementations are consistent.
  • third_party/blink/web_tests/external/wpt/domparsing/DOMParser-parseFromString-xml-scripting-support-disabled.html — Adds a WPT asserting a prefixed xx:script in an SVG-namespaced DOMParser XML document does not execute (window.mark stays 0) after importNode and append.

Background

DOMParser.parseFromString already-started flag — A flag Blink sets on script elements parsed by DOMParser so they are treated as having already run and will not execute when later adopted into a live document.

QualifiedName::Matches vs operator== — operator== compares prefix, local name and namespace, whereas Matches() compares only local name and namespace, ignoring the prefix.

Vulnerability window

  1. Reachability — Reachable from ordinary web content when the DOMParserXmlScriptAlreadyStartedEnabled runtime feature is on; script calls DOMParser().parseFromString with an XML/SVG string.
  2. Trigger — The XML contains a script element with a namespace prefix such as xx:script in the SVG namespace.
  3. Primitive — Pre-patch the prefixed script is not flagged already-started, so it executes when the parsed node is imported/appended into a live document.
  4. Escalation — Arbitrary script execution in the embedding document’s context, i.e. an mXSS/DOM-based script-injection sink (inferred).

Proof of concept

Conceptually: call DOMParser().parseFromString with a text/xml string whose root is SVG-namespaced via a prefix and that contains a prefixed script element setting a global; then importNode and append the root to document.body and observe that pre-patch the script runs (the global is set), whereas post-patch it does not.

Exploitation

  1. Injection — Attacker supplies an XML/SVG string containing a prefixed script element to a sink that parses it with DOMParser.
  2. Adoption — Application imports/appends the parsed subtree into the live document.
  3. Execution — Pre-patch the unflagged prefixed script executes in the document origin (inferred).

Detection & hunting

For defenders and detection engineers:

  • *weggli/grep for QualifiedName comparisons using == against _names::kScriptTag or other tag constants — Flag identity comparisons of QualifiedName where Matches() (prefix-insensitive) is the intended semantics, especially in parser/security-flag code.
  • WPT domparsing scripting-support tests — Ensure prefixed-namespace script variants are covered for both XML parser backends.

Audit directions

  • Other QualifiedName == comparisons against element-name constants across Blink parsers and sanitizers — Audit for places where prefix-sensitive == should be the prefix-insensitive Matches(), which can silently skip security-relevant element handling.

Before / after

Loading diff…