CVE-2026-11150
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 | |
XMLDocumentParserParameterizedTestthird_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc |
modified | |
TEST_Pthird_party/blink/renderer/core/xml/parser/xml_document_parser_test.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/renderer/core/xml/parser/xml_document_parser_test.ccthird_party/blink/web_tests/external/wpt/html/webappapis/dynamic-markup-insertion/the-innerhtml-property/innerhtml-and-xml-namespaces.svg
Patch
From 08c1a0a10f5409571caa073a82159785f349cfba Mon Sep 17 00:00:00 2001 From: Andrew Paseltiner <[email protected]> Date: Mon, 13 Apr 2026 14:20:13 -0700 Subject: [PATCH] Fix nested XML namespace reset bug in fragment parsing This CL fixes a bug in Blink's XML fragment parser where nested null-namespace declarations (xmlns="") were not correctly tracked. This logic was originally introduced in crrev.com/c/7755979 (which addressed crbug.com/463630186). However, that implementation used a single pointer (ancestor_resetting_namespace_) to track the element that triggered a namespace reset, but allowed inner declarations to overwrite it. When an inner reset-triggering element was closed, it would clear the pointer, losing the reset state for the outer ancestor and causing subsequent siblings to incorrectly inherit the context's default namespace. The fix ensures that ancestor_resetting_namespace_ only tracks the outermost reset-triggering element. Any inner resets are ignored since the null namespace is already in effect, and the pointer is only cleared when the outermost resetting element is closed. This CL also adds: - Parameterized regression tests to XMLDocumentParserTest. - A regression test case to the existing WPT suite for XML namespaces. Bug: 463630186 Change-Id: I409d07ced7b59386f87b898335edb2244ff8694f Fixed: 501740299 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7755979 Commit-Queue: Andrew Paseltiner <[email protected]> Reviewed-by: David Baron <[email protected]> Cr-Commit-Position: refs/heads/main@{#1613972} --- 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 a5f7f1a..5af1a04 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 @@ -1172,7 +1172,8 @@ SetAttributes(new_element, prefixed_attributes, GetParserContentPolicy()); - if (parsing_fragment_ && encountered_namespace_reset) { + if (parsing_fragment_ && encountered_namespace_reset && + !ancestor_resetting_namespace_) { ancestor_resetting_namespace_ = new_element; } 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 396dca4..08c5dd4 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 @@ -488,7 +488,8 @@ SetAttributes(new_element, prefixed_attributes, GetParserContentPolicy()); - if (parsing_fragment_ && encountered_namespace_reset) { + if (parsing_fragment_ && encountered_namespace_reset && + !ancestor_resetting_namespace_) { ancestor_resetting_namespace_ = new_element; } diff --git a/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc b/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc index 35ae8b96..a079628 100644 --- a/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc +++ b/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc @@ -13,6 +13,7 @@ #include "third_party/blink/renderer/platform/bindings/exception_state.h" #include "third_party/blink/renderer/platform/heap/garbage_collected.h" #include "third_party/blink/renderer/platform/runtime_enabled_features.h" +#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" #include "third_party/blink/renderer/platform/testing/task_environment.h" #include "third_party/blink/renderer/platform/wtf/text/atomic_string.h" @@ -70,4 +71,89 @@ EXPECT_EQ(bar->localName(), "foo:bar"); } +class XMLDocumentParserParameterizedTest + : public testing::Test, + public testing::WithParamInterface<bool> { + public: + XMLDocumentParserParameterizedTest() : scoped_rust_(GetParam()) {} + + protected: + ScopedXMLParsingRustForTest scoped_rust_; +}; + +INSTANTIATE_TEST_SUITE_P(All, + XMLDocumentParserParameterizedTest, + testing::Bool()); + +// crbug.com/501740299 +TEST_P(XMLDocumentParserParameterizedTest, SingleNamespaceReset) { + test::TaskEnvironment task_environment; + ScopedNullExecutionContext execution_context; + execution_context.GetExecutionContext().SetUpSecurityContextForTesting(); + auto& doc = *Document::CreateForTest(execution_context.GetExecutionContext()); + + // Create an XHTML context element. + DummyExceptionStateForTesting exception; + const AtomicString xhtml_ns("http://www.w3.org/1999/xhtml"); + auto* div = doc.createElementNS(xhtml_ns, AtomicString("div"), exception); + ASSERT_TRUE(div); + + DocumentFragment* fragment = DocumentFragment::Create(doc); + // Payload with a single xmlns="". + const char* payload = "<a xmlns=''><iframe/></a>"; + EXPECT_TRUE(fragment->ParseXML(payload, div, ASSERT_NO_EXCEPTION)); + + auto* a = To<Element>(fragment->firstChild()); + ASSERT_TRUE(a); + EXPECT_EQ(a->namespaceURI(), g_null_atom); + + auto* iframe = To<Element>(a->firstChild()); + ASSERT_TRUE(iframe); + EXPECT_EQ(iframe->localName(), "iframe"); + EXPECT_EQ(iframe->namespaceURI(), g_null_atom) + << "iframe should be in null namespace inherited from <a>"; +} + +// crbug.com/501740299 +TEST_P(XMLDocumentParserParameterizedTest, NestedNamespaceReset) { + test::TaskEnvironment task_environment; + ScopedNullExecutionContext execution_context; + execution_context.GetExecutionContext().SetUpSecurityContextForTesting(); + auto& doc = *Document::CreateForTest(execution_context.GetExecutionContext()); + + // Create an XHTML context element. + DummyExceptionStateForTesting exception; + const AtomicString xhtml_ns("http://www.w3.org/1999/xhtml"); + auto* div = doc.createElementNS(xhtml_ns, AtomicString("div"), exception); + ASSERT_TRUE(div); + + DocumentFragment* fragment = DocumentFragment::Create(doc); + // Payload with nested xmlns="". + // The outer <a> resets the namespace to null. + // The inner <b> also resets the namespace to null. + // Verification that closing </b> doesn't clear the reset state for outer <a>. + const char* payload = "<a xmlns=''><b xmlns=''>x</b><iframe/></a>"; + EXPECT_TRUE(fragment->ParseXML(payload, div, ASSERT_NO_EXCEPTION)); + + // Structure: fragment -> <a> -> [<b>, <iframe>] + auto* a = To<Element>(fragment->firstChild()); + ASSERT_TRUE(a); + EXPECT_EQ(a->localName(), "a"); + EXPECT_EQ(a->namespaceURI(), g_null_atom); + + auto* b = To<Element>(a->firstChild()); + ASSERT_TRUE(b); + EXPECT_EQ(b->localName(), "b"); + EXPECT_EQ(b->namespaceURI(), g_null_atom); + + auto* iframe = To<Element>(b->nextSibling()); + ASSERT_TRUE(iframe); + EXPECT_EQ(iframe->localName(), "iframe"); + + // Verify that the iframe correctly inherits the null namespace from <a>, + // ensuring that the inner <b> declaration didn't clobber the reset state. + EXPECT_EQ(iframe->namespaceURI(), g_null_atom) + << "iframe should be in null namespace due to outer <a> reset"; +} + } // namespace blink 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 2317ddc20..06ab1cc 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,15 @@ }, "default namespace applied to sibling of namespace-resetting 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); + assert_equals(prefixedContainer.firstChild.firstChild.firstChild.namespaceURI, null); + assert_equals(prefixedContainer.firstChild.firstChild.lastChild.namespaceURI, null); + assert_equals(prefixedContainer.firstChild.lastChild.namespaceURI, SVG_NS); + }, "nested default namespace reset works inside parse of fragment"); + + test(() => { prefixedContainer.innerHTML = "<e><h:f xmlns:h='https://example.com/new-h'><g><h:d></h:d></g></h:f></e>"; assert_equals(prefixedContainer.firstChild.firstChild.namespaceURI, "https://example.com/new-h"); assert_equals(prefixedContainer.firstChild.firstChild.firstChild.namespaceURI, SVG_NS);
Regression Test / PoC
diff --git a/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc b/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc
index 35ae8b96..a079628 100644
--- a/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc
+++ b/third_party/blink/renderer/core/xml/parser/xml_document_parser_test.cc
@@ -13,6 +13,7 @@
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
+#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"
#include "third_party/blink/renderer/platform/testing/task_environment.h"
#include "third_party/blink/renderer/platform/wtf/text/atomic_string.h"
@@ -70,4 +71,89 @@
EXPECT_EQ(bar->localName(), "foo:bar");
}
+class XMLDocumentParserParameterizedTest
+ : public testing::Test,
+ public testing::WithParamInterface<bool> {
+ public:
+ XMLDocumentParserParameterizedTest() : scoped_rust_(GetParam()) {}
+
+ protected:
+ ScopedXMLParsingRustForTest scoped_rust_;
+};
+
+INSTANTIATE_TEST_SUITE_P(All,
+ XMLDocumentParserParameterizedTest,
+ testing::Bool());
+
+// crbug.com/501740299
+TEST_P(XMLDocumentParserParameterizedTest, SingleNamespaceReset) {
+ test::TaskEnvironment task_environment;
+ ScopedNullExecutionContext execution_context;
+ execution_context.GetExecutionContext().SetUpSecurityContextForTesting();
+ auto& doc = *Document::CreateForTest(execution_context.GetExecutionContext());
+
+ // Create an XHTML context element.
+ DummyExceptionStateForTesting exception;
+ const AtomicString xhtml_ns("http://www.w3.org/1999/xhtml");
+ auto* div = doc.createElementNS(xhtml_ns, AtomicString("div"), exception);
+ ASSERT_TRUE(div);
+
+ DocumentFragment* fragment = DocumentFragment::Create(doc);
+ // Payload with a single xmlns="".
+ const char* payload = "<a xmlns=''><iframe/></a>";
+ EXPECT_TRUE(fragment->ParseXML(payload, div, ASSERT_NO_EXCEPTION));
+
+ auto* a = To<Element>(fragment->firstChild());
+ ASSERT_TRUE(a);
+ EXPECT_EQ(a->namespaceURI(), g_null_atom);
+
+ auto* iframe = To<Element>(a->firstChild());
+ ASSERT_TRUE(iframe);
+ EXPECT_EQ(iframe->localName(), "iframe");
+ EXPECT_EQ(iframe->namespaceURI(), g_null_atom)
+ << "iframe should be in null namespace inherited from <a>";
+}
+
+// crbug.com/501740299
+TEST_P(XMLDocumentParserParameterizedTest, NestedNamespaceReset) {
+ test::TaskEnvironment task_environment;
+ ScopedNullExecutionContext execution_context;
+ execution_context.GetExecutionContext().SetUpSecurityContextForTesting();
+ auto& doc = *Document::CreateForTest(execution_context.GetExecutionContext());
+
+ // Create an XHTML context element.
+ DummyExceptionStateForTesting exception;
+ const AtomicString xhtml_ns("http://www.w3.org/1999/xhtml");
+ auto* div = doc.createElementNS(xhtml_ns, AtomicString("div"), exception);
+ ASSERT_TRUE(div);
+
+ DocumentFragment* fragment = DocumentFragment::Create(doc);
+ // Payload with nested xmlns="".
+ // The outer <a> resets the namespace to null.
+ // The inner <b> also resets the namespace to null.
+ // Verification that closing </b> doesn't clear the reset state for outer <a>.
+ const char* payload = "<a xmlns=''><b xmlns=''>x</b><iframe/></a>";
+ EXPECT_TRUE(fragment->ParseXML(payload, div, ASSERT_NO_EXCEPTION));
+
+ // Structure: fragment -> <a> -> [<b>, <iframe>]
+ auto* a = To<Element>(fragment->firstChild());
+ ASSERT_TRUE(a);
+ EXPECT_EQ(a->localName(), "a");
+ EXPECT_EQ(a->namespaceURI(), g_null_atom);
+
+ auto* b = To<Element>(a->firstChild());
+ ASSERT_TRUE(b);
+ EXPECT_EQ(b->localName(), "b");
+ EXPECT_EQ(b->namespaceURI(), g_null_atom);
+
+ auto* iframe = To<Element>(b->nextSibling());
+ ASSERT_TRUE(iframe);
+ EXPECT_EQ(iframe->localName(), "iframe");
+
+ // Verify that the iframe correctly inherits the null namespace from <a>,
+ // ensuring that the inner <b> declaration didn't clobber the reset state.
+ EXPECT_EQ(iframe->namespaceURI(), g_null_atom)
+ << "iframe should be in null namespace due to outer <a> reset";
+}
+
} // namespace blink
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 2317ddc20..06ab1cc 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,15 @@
}, "default namespace applied to sibling of namespace-resetting 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);
+ assert_equals(prefixedContainer.firstChild.firstChild.firstChild.namespaceURI, null);
+ assert_equals(prefixedContainer.firstChild.firstChild.lastChild.namespaceURI, null);
+ assert_equals(prefixedContainer.firstChild.lastChild.namespaceURI, SVG_NS);
+ }, "nested default namespace reset works inside parse of fragment");
+
+ test(() => {
prefixedContainer.innerHTML = "<e><h:f xmlns:h='https://example.com/new-h'><g><h:d></h:d></g></h:f></e>";
assert_equals(prefixedContainer.firstChild.firstChild.namespaceURI, "https://example.com/new-h");
assert_equals(prefixedContainer.firstChild.firstChild.firstChild.namespaceURI, SVG_NS);
Original Bug Report
Namespace Confusion in XML Fragment Parsing via Nested xmlns=""
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 without the Chrome Security team.
Overview: A logic error in Chromium’s XML fragment parser fails to correctly track nested null-namespace declarations (xmlns="") because it uses a single pointer instead of a stack. When an inner element with a null namespace is closed, the parser erroneously clears the namespace reset state, causing subsequent sibling elements to incorrectly inherit the context’s default namespace (e.g., XHTML). This namespace confusion provides a potential mutation XSS (mXSS) primitive that can bypass XML-aware HTML sanitizers.
Affected files:
third_party/blink/renderer/core/xml/parser/xml_document_parser.ccthird_party/blink/renderer/core/xml/parser/xml_document_parser_rs.ccthird_party/blink/renderer/core/xml/parser/xml_document_parser.hthird_party/blink/renderer/core/xml/parser/xml_document_parser_rs.h
Estimated timestamp from git blame: 2025-12-02
Description
There is a potential namespace confusion vulnerability in Blink’s XML fragment parsing logic (XMLDocumentParser and its Rust equivalent XMLDocumentParserRs). When injecting markup via fragment APIs like innerHTML, Blink attempts to fix up namespace URIs that libxml2 leaves blank.
To prevent the context’s default namespace (e.g., the XHTML namespace) from incorrectly applying to elements that explicitly reset the namespace to null via xmlns="", the parser tracks the ancestor element that triggered the reset using a member variable: ancestor_resetting_namespace_.
However, this member is implemented as a single pointer rather than a stack. If an attacker nests elements that declare xmlns="", the inner element overwrites ancestor_resetting_namespace_. When the inner element is closed, EndElementNs clears the pointer to nullptr. This permanently loses the state of the outer namespace-resetting element. As a result, subsequent sibling elements that should be in the null namespace are erroneously promoted to the context’s default namespace (e.g., http://www.w3.org/1999/xhtml), instantiating them as live HTML elements rather than inert XML elements.
Affected Code
third_party/blink/renderer/core/xml/parser/xml_document_parser.ccthird_party/blink/renderer/core/xml/parser/xml_document_parser_rs.cc
In both files, StartElementNs overwrites the pointer without saving the previous state:
if (parsing_fragment_ && encountered_namespace_reset) {
ancestor_resetting_namespace_ = new_element;
}
And EndElementNs blindly clears it:
if (ancestor_resetting_namespace_ == n) {
ancestor_resetting_namespace_ = nullptr;
}
Potential Attacker Steps
Note: These are suggested steps based on static code analysis. Our tooling agent does not currently possess the ability to execute code or run a live proof-of-concept.
- An attacker crafts a payload containing nested
xmlns=""declarations, for example:<a xmlns=""><b xmlns="">x</b><iframe srcdoc="<script>alert(document.domain)</script>"></iframe></a> - The payload is supplied to a web application that sanitizes input using an XML-aware sanitizer (like DOMPurify).
- The sanitizer parses the payload as a full document. Full document parsing relies on
libxml2’s robust namespace tracking (bypassing Blink’s fragment fixup logic). The sanitizer correctly observes the<iframe>as an inert XML element in the null namespace and permits thesrcdocattribute to pass through unmodified. - The application takes the sanitized string and injects it into an active XML/XHTML document using a fragment parsing API, such as
document.getElementById('target').innerHTML = sanitized_string;, where the target element possesses an XHTML default namespace. - During fragment parsing,
<b>overwritesancestor_resetting_namespace_and subsequently clears it upon closing. - When the parser reaches the
<iframe>,ancestor_resetting_namespace_isnullptr. The parser incorrectly falls back to the target’s default XHTML namespace, instantiating an executableHTMLIFrameElement. - The
srcdocscript executes in the context of the application’s domain, resulting in Mutation XSS (mXSS).
Suggested Fix
Replace the single Member<ContainerNode> ancestor_resetting_namespace_ pointer with a stack (e.g., HeapVector<Member<ContainerNode>>), or track the depth/count of namespace resets.
- In
StartElementNs, if a namespace reset is encountered, push the new element onto the stack. - When checking for namespace resets during URI fixup, evaluate if the stack is non-empty.
- In
EndElementNs, if the closing element matches the top of the stack, pop it off, thereby restoring the previous namespace-resetting context.
Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b
Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.