CVE-2026-14000
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/xml/parser/xml_document_parser.cc |
modified | |
ifthird_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc |
modified |
Files Changed
third_party/blink/renderer/core/xml/parser/xml_document_parser.ccthird_party/blink/renderer/core/xml/parser/xml_document_parser_rs.ccthird_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg
Patch
From 78c5c5a275325f808acac45205e2bfd4ac9ce4aa Mon Sep 17 00:00:00 2001 From: David Baron <[email protected]> Date: Thu, 21 May 2026 05:05:10 -0700 Subject: [PATCH] Fix namespace resets on html:template elements in XML fragment parsing. Fixed: 514461552 Change-Id: I26553b672a0da9e1937af3bd877ec64b11d1cb85 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7862113 Reviewed-by: Dominik Röttsches <[email protected]> Commit-Queue: David Baron <[email protected]> Cr-Commit-Position: refs/heads/main@{#1634188} --- diff --git a/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc b/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc index 1957e77..bf19c392 100644 --- a/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc +++ b/third_party/blink/renderer/core/xml/parser/xml_document_parser.cc @@ -1225,6 +1225,15 @@ ContainerNode* n = current_node_; auto* element = DynamicTo<Element>(n); if (!element) { + // Check if the current node is the DocumentFragment for an + // HTMLTemplateElement that is ancestor_resetting_namespace_. + if (auto* resetting_template = DynamicTo<HTMLTemplateElement>( + ancestor_resetting_namespace_.Get())) { + if (resetting_template->content() == current_node_) { + ancestor_resetting_namespace_ = nullptr; + } + } + PopCurrentNode(); return; } diff --git a/third_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc b/third_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc index 08c5dd4..4d92fdd 100644 --- a/third_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc +++ b/third_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc @@ -536,6 +536,15 @@ ContainerNode* n = current_node_; auto* element = DynamicTo<Element>(n); if (!element) { + // Check if the current node is the DocumentFragment for an + // HTMLTemplateElement that is ancestor_resetting_namespace_. + if (auto* resetting_template = DynamicTo<HTMLTemplateElement>( + ancestor_resetting_namespace_.Get())) { + if (resetting_template->content() == current_node_) { + ancestor_resetting_namespace_ = nullptr; + } + } + PopCurrentNode(); return; } diff --git a/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg index 06ab1cc..1e95d69 100644 --- a/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg +++ b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg @@ -86,6 +86,12 @@ }, "default namespace applied to sibling of namespace-resetting element in parse of fragment."); test(() => { + prefixedContainer.innerHTML = "<e><h:template xmlns=''><g></g></h:template><h/></e>"; + assert_equals(prefixedContainer.firstChild.firstChild.content.firstChild.namespaceURI, null); + assert_equals(prefixedContainer.firstChild.lastChild.namespaceURI, SVG_NS); + }, "default namespace applied to sibling of namespace-resetting template element in parse of fragment."); + + test(() => { prefixedContainer.innerHTML = "<e><f xmlns=''><g xmlns=''></g><h/></f><i></i></e>"; assert_equals(prefixedContainer.firstChild.namespaceURI, SVG_NS); assert_equals(prefixedContainer.firstChild.firstChild.namespaceURI, null);
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg
index 06ab1cc..1e95d69 100644
--- a/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg
+++ b/third_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg
@@ -86,6 +86,12 @@
}, "default namespace applied to sibling of namespace-resetting element in parse of fragment.");
test(() => {
+ prefixedContainer.innerHTML = "<e><h:template xmlns=''><g></g></h:template><h/></e>";
+ assert_equals(prefixedContainer.firstChild.firstChild.content.firstChild.namespaceURI, null);
+ assert_equals(prefixedContainer.firstChild.lastChild.namespaceURI, SVG_NS);
+ }, "default namespace applied to sibling of namespace-resetting template element in parse of fragment.");
+
+ test(() => {
prefixedContainer.innerHTML = "<e><f xmlns=''><g xmlns=''></g><h/></f><i></i></e>";
assert_equals(prefixedContainer.firstChild.namespaceURI, SVG_NS);
assert_equals(prefixedContainer.firstChild.firstChild.namespaceURI, null);
Original Bug Report
Potential namespace confusion in XMLDocumentParser via template elements
Flapjack, 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: A logic error in Blink’s XML fragment parser fails to clear the namespace reset state when closing an HTML <template> element. This causes subsequent sibling elements to incorrectly inherit a null namespace instead of the default namespace. This potential vulnerability provides a mutation XSS (mXSS) primitive that can bypass DOM-based sanitizers.
Affected files:
third_party/blink/renderer/core/xml/parser/xml_document_parser.ccthird_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Description
There is a potential logic error in XMLDocumentParser (and its Rust counterpart XMLDocumentParserRs) regarding how it tracks namespace resets during XML fragment parsing.
When parsing an XML fragment, the parser uses ancestor_resetting_namespace_ to track the outermost element that reset the default namespace to null (e.g., via xmlns="").
In XMLDocumentParser::StartElementNs, if the parser encounters an HTMLTemplateElement, it pushes the template’s content (a DocumentFragment) onto its internal node stack instead of the template element itself:
if (auto* template_element = DynamicTo<HTMLTemplateElement>(*new_element))
PushCurrentNode(template_element->content());
However, ancestor_resetting_namespace_ is correctly set to the HTMLTemplateElement if it performed a namespace reset.
In XMLDocumentParser::EndElementNs, the parser attempts to clear this tracking variable when the resetting element is closed. It does this by checking if the currently closing node n matches ancestor_resetting_namespace_:
ContainerNode* n = current_node_;
auto* element = DynamicTo<Element>(n);
if (!element) {
PopCurrentNode();
return;
}
if (ancestor_resetting_namespace_ == n) {
ancestor_resetting_namespace_ = nullptr;
}
When the </template> tag is reached, n is the DocumentFragment. Because a DocumentFragment is not an Element, the DynamicTo<Element>(n) cast fails. The function calls PopCurrentNode() and returns early. This completely bypasses the cleanup logic. Consequently, ancestor_resetting_namespace_ remains set, and all subsequent sibling elements of the <template> incorrectly inherit a null namespace.
Potential Exploitation Steps
(Note: These are suggested steps based on static analysis, as our tooling cannot currently run a live proof-of-concept).
An attacker could exploit this bug against an application that parses untrusted input as XML, sanitizes the DOM, and serializes it into an HTML context:
- The attacker provides a crafted XML fragment:
<html:template xmlns:html="http://www.w3.org/1999/xhtml" xmlns=""></html:template><script>alert(1)</script>. - The
html:prefix ensures the template is created as anHTMLTemplateElement(pushing aDocumentFragmentto the stack), whilexmlns=""explicitly triggers the namespace reset. - The parser fails to clear
ancestor_resetting_namespace_when the template closes. - The parser encounters the
<script>tag. Because the namespace reset state leaked, the<script>tag is instantiated in the null namespace as a genericElement, rather than anHTMLScriptElement. - A DOM-based sanitizer inspects the tree. Seeing a generic
<script>element in the null namespace, it may assume it is an inert XML tag and allow it. - The application serializes the DOM (e.g., using
XMLSerializer). The serializer notices the null namespace and synthesizes a namespace declaration:<script xmlns="">alert(1)</script>. - The serialized string is injected into an HTML document. The HTML parser ignores the
xmlnsattribute, parses it as a live HTML script, and executes the payload, achieving Cross-Site Scripting (XSS).
Suggested Fix
Modify the cleanup logic in XMLDocumentParser::EndElementNs (and xml_document_parser_rs.cc) to account for <template> elements pushing a DocumentFragment.
One potential approach is to check if ancestor_resetting_namespace_ is a template element, and if the node being popped is that template’s content:
ContainerNode* n = current_node_;
// Add cleanup logic before the early return
if (ancestor_resetting_namespace_ == n ||
(ancestor_resetting_namespace_ &&
ancestor_resetting_namespace_->IsTemplateElement() &&
n == static_cast<HTMLTemplateElement*>(ancestor_resetting_namespace_.Get())->content())) {
ancestor_resetting_namespace_ = nullptr;
}
auto* element = DynamicTo<Element>(n);
if (!element) {
PopCurrentNode();
return;
}
Alternatively, ensure ancestor_resetting_namespace_ is properly cleared whenever PopCurrentNode() corresponds to exiting the scope of the resetting element.
Evaluated with Chrome root at commit: b7d0c4d810da1b31400f198c70d9720fc8f0e5a0
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.