CVE-2026-13977
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
HTMLStackItemthird_party/blink/renderer/core/html/parser/html_stack_item.h |
modified | |
switchthird_party/blink/renderer/core/html/parser/html_stack_item.h |
modified | |
is_document_fragment_node_third_party/blink/renderer/core/html/parser/html_stack_item.h |
modified |
Files Changed
third_party/blink/renderer/core/html/parser/html_stack_item.hthird_party/blink/renderer/core/html/parser/html_tree_builder.ccthird_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html
Patch
From 2050441520a7c77c2c15c46564548a522b37f885 Mon Sep 17 00:00:00 2001
From: David Baron <dbaron@chromium.org>
Date: Thu, 21 May 2026 19:05:06 -0700
Subject: [PATCH] Fix fragment parsing inside <mathml:annotation-xml> elements.
Fixed: 513859894
Change-Id: I413d5b1fdb61f60e38ca77d25a8b463c630f003e
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7863551
Reviewed-by: Mason Freed <masonf@chromium.org>
Commit-Queue: David Baron <dbaron@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1634672}
---
diff --git a/third_party/blink/renderer/core/html/parser/html_stack_item.h b/third_party/blink/renderer/core/html/parser/html_stack_item.h
index 6419244..72ae202 100644
--- a/third_party/blink/renderer/core/html/parser/html_stack_item.h
+++ b/third_party/blink/renderer/core/html/parser/html_stack_item.h
@@ -45,19 +45,37 @@
// also saves a little bit of memory, as a side effect.)
class HTMLStackItem final : public GarbageCollected<HTMLStackItem> {
public:
- enum ItemType { kItemForContextElement, kItemForDocumentFragmentNode };
+ // These enums used to be needed to disambiguate constructors. They aren't
+ // needed any more, but they still help document the constructor calls more
+ // clearly.
+ enum DocumentFragmentItemType { kItemForDocumentFragmentNode };
+ enum ContextElementItemType { kItemForContextElement };
- HTMLStackItem(ContainerNode* node, ItemType type)
- : node_(node), token_name_(html_names::HTMLTag::kUnknown) {
- switch (type) {
- case kItemForDocumentFragmentNode:
- is_document_fragment_node_ = true;
- break;
- case kItemForContextElement:
- token_name_ = HTMLTokenName::FromLocalName(GetElement()->localName());
- namespace_uri_ = GetElement()->namespaceURI();
- is_document_fragment_node_ = false;
- break;
+ HTMLStackItem(ContainerNode* node, DocumentFragmentItemType)
+ : node_(node),
+ token_name_(html_names::HTMLTag::kUnknown),
+ is_document_fragment_node_(true) {}
+
+ // You cannot call this constructor directly (but it must be public so that
+ // MakeGarbageCollected() can); use CreateForContextElement() below instead.
+ HTMLStackItem(base::PassKey<HTMLStackItem>,
+ Element* element,
+ ContextElementItemType)
+ : node_(element),
+ token_name_(HTMLTokenName::FromLocalName(element->localName())),
+ namespace_uri_(element->namespaceURI()),
+ num_token_attributes_(element->Attributes().size()),
+ is_document_fragment_node_(false) {
+ // We need to store the attributes because we sometimes make decisions
+ // based on attributes of the context elements, for example the encoding
+ // attribute of <mathml:annotation-xml> elements.
+ //
+ // We rely on Create() allocating extra memory past our end for the
+ // attributes.
+ const AttributeCollection element_attributes = element->Attributes();
+ auto attributes = TokenAttributesSpan();
+ for (wtf_size_t i = 0; i < element_attributes.size(); ++i) {
+ new (&attributes[i]) Attribute(element_attributes[i]);
}
}
@@ -99,6 +117,12 @@
base::PassKey<HTMLStackItem>(), node, token, namespace_uri);
}
+ static HTMLStackItem* CreateForContextElement(Element* element) {
+ return MakeGarbageCollected<HTMLStackItem>(
+ AdditionalBytes(element->Attributes().size() * sizeof(Attribute)),
+ base::PassKey<HTMLStackItem>(), element, kItemForContextElement);
+ }
+
Element* GetElement() const { return To<Element>(node_.Get()); }
ContainerNode* GetNode() const { return node_.Get(); }
diff --git a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
index fa8347dd..a65b657c 100644
--- a/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
+++ b/third_party/blink/renderer/core/html/parser/html_tree_builder.cc
@@ -423,8 +423,8 @@
DCHECK(fragment_target);
DCHECK(!fragment_target->HasChildren());
fragment_target_ = fragment_target;
- context_element_stack_item_ = MakeGarbageCollected<HTMLStackItem>(
- context_element, HTMLStackItem::kItemForContextElement);
+ context_element_stack_item_ =
+ HTMLStackItem::CreateForContextElement(context_element);
}
void HTMLTreeBuilder::FragmentParsingContext::Trace(Visitor* visitor) const {
diff --git a/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html
new file mode 100644
index 0000000..451c479
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<title>mathml:annotation-xml and fragment parsing</title>
+<link rel="help" href="https://issues.chromium.org/513859894">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<math>
+ <annotation-xml id="noenc"></annotation-xml>
+ <annotation-xml id="enchtml" encoding="application/xhtml+xml"></annotation-xml>
+</math>
+
+<script>
+ "use strict";
+
+ const noenc = document.getElementById("noenc");
+ const enchtml = document.getElementById("enchtml");
+
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+ const MATH_NS = "http://www.w3.org/1998/Math/MathML";
+
+ test(() => {
+ noenc.innerHTML = "<style><img></style>";
+ assert_equals(noenc.namespaceURI, MATH_NS, "<annotation-xml> namespace");
+ assert_equals(noenc.childNodes.length, 2, "<annotation-xml> child count");
+ assert_equals(noenc.firstChild.namespaceURI, MATH_NS, "<annotation-xml> first child namespace");
+ assert_equals(noenc.firstChild.localName, "style", "<annotation-xml> first child localName");
+ assert_equals(noenc.firstChild.childNodes.length, 0, "<annotation-xml> first child child count");
+ assert_equals(noenc.firstChild.nextSibling.namespaceURI, HTML_NS, "<annotation-xml> second child namespace");
+ assert_equals(noenc.firstChild.nextSibling.localName, "img", "<annotation-xml> second child localName");
+ assert_equals(noenc.firstChild.nextSibling.childNodes.length, 0, "<annotation-xml> second child child count");
+ }, `HTML parsing doesn't apply in <annotation-xml> without encoding attribute`);
+
+ test(() => {
+ enchtml.innerHTML = "<style><img></style>";
+ assert_equals(enchtml.namespaceURI, MATH_NS, "<annotation-xml> namespace");
+ assert_equals(enchtml.childNodes.length, 1, "<annotation-xml> child count");
+ assert_equals(enchtml.firstChild.namespaceURI, HTML_NS, "<annotation-xml> child namespace");
+ assert_equals(enchtml.firstChild.childNodes.length, 1, "<annotation-xml> child's child count");
+ assert_equals(enchtml.firstChild.childNodes[0].nodeType, Node.TEXT_NODE, "<annotation-xml> grandchild node type");
+ }, `HTML parsing does apply in <annotation-xml encoding="application/xhtml+xml">`);
+
+</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html
new file mode 100644
index 0000000..451c479
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/annotation-xml-fragment-parsing.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<title>mathml:annotation-xml and fragment parsing</title>
+<link rel="help" href="https://issues.chromium.org/513859894">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+
+<math>
+ <annotation-xml id="noenc"></annotation-xml>
+ <annotation-xml id="enchtml" encoding="application/xhtml+xml"></annotation-xml>
+</math>
+
+<script>
+ "use strict";
+
+ const noenc = document.getElementById("noenc");
+ const enchtml = document.getElementById("enchtml");
+
+ const HTML_NS = "http://www.w3.org/1999/xhtml";
+ const MATH_NS = "http://www.w3.org/1998/Math/MathML";
+
+ test(() => {
+ noenc.innerHTML = "<style><img></style>";
+ assert_equals(noenc.namespaceURI, MATH_NS, "<annotation-xml> namespace");
+ assert_equals(noenc.childNodes.length, 2, "<annotation-xml> child count");
+ assert_equals(noenc.firstChild.namespaceURI, MATH_NS, "<annotation-xml> first child namespace");
+ assert_equals(noenc.firstChild.localName, "style", "<annotation-xml> first child localName");
+ assert_equals(noenc.firstChild.childNodes.length, 0, "<annotation-xml> first child child count");
+ assert_equals(noenc.firstChild.nextSibling.namespaceURI, HTML_NS, "<annotation-xml> second child namespace");
+ assert_equals(noenc.firstChild.nextSibling.localName, "img", "<annotation-xml> second child localName");
+ assert_equals(noenc.firstChild.nextSibling.childNodes.length, 0, "<annotation-xml> second child child count");
+ }, `HTML parsing doesn't apply in <annotation-xml> without encoding attribute`);
+
+ test(() => {
+ enchtml.innerHTML = "<style><img></style>";
+ assert_equals(enchtml.namespaceURI, MATH_NS, "<annotation-xml> namespace");
+ assert_equals(enchtml.childNodes.length, 1, "<annotation-xml> child count");
+ assert_equals(enchtml.firstChild.namespaceURI, HTML_NS, "<annotation-xml> child namespace");
+ assert_equals(enchtml.firstChild.childNodes.length, 1, "<annotation-xml> child's child count");
+ assert_equals(enchtml.firstChild.childNodes[0].nodeType, Node.TEXT_NODE, "<annotation-xml> grandchild node type");
+ }, `HTML parsing does apply in <annotation-xml encoding="application/xhtml+xml">`);
+
+</script>
Original Bug Report
MXSS in HTML fragment parser due to attribute loss on context element
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Blink’s HTML fragment parser fails to preserve attributes of the context element in its internal stack representation. This causes MathML <annotation-xml> elements with specific encodings to be incorrectly treated as non-integration points, leading to a parser divergence that enables mutation XSS (mXSS).
Affected files:
third_party/blink/renderer/core/html/parser/html_stack_item.hthird_party/blink/renderer/core/html/parser/html_tree_builder.ccthird_party/blink/renderer/core/html/parser/html_element_stack.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A potential vulnerability in Blink’s HTML fragment parser may allow for mutation XSS (mXSS). The parser fails to copy attributes from the context element when initializing the parsing stack for fragment parsing (e.g., via innerHTML or setHTMLUnsafe). This can lead to a divergence in how HTML integration points are identified, allowing malicious markup to bypass sanitization.
Root Cause Analysis
When performing fragment parsing, the context element is represented in the parser’s stack by an HTMLStackItem. The constructor used for this purpose (kItemForContextElement) in third_party/blink/renderer/core/html/parser/html_stack_item.h neglects to copy the attributes of the element:
// third_party/blink/renderer/core/html/parser/html_stack_item.h
HTMLStackItem(ContainerNode* node, ItemType type)
: node_(node), token_name_(html_names::HTMLTag::kUnknown) {
switch (type) {
case kItemForContextElement:
token_name_ = HTMLTokenName::FromLocalName(GetElement()->localName());
namespace_uri_ = GetElement()->namespaceURI();
is_document_fragment_node_ = false;
// Attributes are not copied here, and num_token_attributes_ remains 0.
break;
// ...
}
}
Because attributes are missing, the check in HTMLElementStack::IsHTMLIntegrationPoint fails for MathML <annotation-xml> elements. This check depends on the encoding attribute to determine if the element should be treated as an HTML integration point:
// third_party/blink/renderer/core/html/parser/html_element_stack.cc
bool HTMLElementStack::IsHTMLIntegrationPoint(HTMLStackItem* item) {
if (item->HasTagName(mathml_names::kAnnotationXmlTag)) {
Attribute* encoding_attr = item->GetAttributeItem(mathml_names::kEncodingAttr);
if (encoding_attr) {
// ... check for text/html or application/xhtml+xml
}
return false; // Always taken if attributes were not preserved
}
// ...
}
Security Impact
When IsHTMLIntegrationPoint incorrectly returns false, the parser may remain in “Foreign Content” mode. In this mode, HTML-specific tokenizer state transitions are bypassed. For example, a <style> tag inserted into this context will not trigger the RAWTEXT tokenizer state. Subsequent content, such as <img src=x onerror=...> , will then be parsed as live HTML elements with active event handlers rather than inert text inside the style block.
This creates an mXSS primitive where a sanitizer might correctly identify an integration point and deem the markup safe, but Blink’s fragment parser divergence causes it to materialize executable code upon insertion.
Potential Reproduction Steps
Note: These steps are based on code analysis and have not been verified with a live proof-of-concept.
- Create a MathML
<annotation-xml>element and set itsencodingattribute to"text/html". - Append this element to the document.
- Assign the following string to its
innerHTML:'<style><img src=x onerror=alert(document.domain)></style>'. - If the vulnerability exists, the
onerrorhandler will fire because the<img>tag is parsed as a live element instead of text within the<style>block.
const ax = document.createElementNS('http://www.w3.org/1998/Math/MathML','annotation-xml');
ax.setAttribute('encoding','text/html');
document.body.append(ax);
ax.innerHTML = '<style><img src=x onerror=alert(document.domain)></style>';
Suggested Fix
Modify the HTMLStackItem initialization for the fragment context element to correctly preserve attributes. This involves calculating the number of attributes on the context element and using Blink’s AdditionalBytes allocation mechanism to store them inline in the HTMLStackItem, similar to how the Create method handles tokens.
Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.