Medium patch-now ~ plausible CVSS 8.8 chrome Type Confusion 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType confusion in XML
DescriptionType confusion in XML
ComponentXML
Bug ClassType Confusion
Action tierpatch-now — corruption primitive, web-reachable in default config (plausible) — upheld by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: Diff confirms the NULL-URI comparison pushing the stylesheet doc. But the claimed invariant (node-sets hold only source-tree nodes) is unsupported: document() legitimately returns other documents' nodes; the diff does not show where the stylesheet node is misinterpreted.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495541478
Fix commit8005fbd4ba53 (chromium/src) +63/-8
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-23 (per vendor advisory)
Fix landed2026-08-01 (public git)
Disclosed2026-09-08
Patch gap38 days — fix public in source before the advisory

Background

`document()` function
an XSLT/XPath function that loads an external XML resource and returns its nodes as a node-set, with the idiom document('') conventionally selecting the current stylesheet’s own document.
`xsltDocumentFunction`
the libxslt C implementation of document() in functions.c, which resolves the argument URI against a base URI and pushes the resulting node-set onto the XPath value stack via valuePush.
Base URI resolution
computing an absolute URI from a reference and a base with xmlBuildURI; when the base is malformed (e.g. http://[, an unterminated IPv6 literal) resolution fails and yields NULL.
Type confusion
treating an object as a type it is not; here, injecting the stylesheet’s own document node into a node-set that is being evaluated over a different tree (the source document).

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Diff confirms the NULL-URI comparison pushing the stylesheet doc. But the claimed invariant (node-sets hold only source-tree nodes) is unsupported: document() legitimately returns other documents' nodes; the diff does not show where the stylesheet node is misinterpreted.

The vulnerable code path is the URI == NULL branch of xsltDocumentFunction, which is entered when the second-argument node’s xml:base is invalid and xmlBuildURI therefore returns NULL. On that failure path the old code tried to detect the document('')-selects-stylesheet idiom by evaluating xmlStrEqual(URI, tctxt->style->doc->URL) — but URI is already known to be NULL inside this branch, so the comparison is degenerate and, when the stylesheet document’s URL is also NULL (as it is for a stylesheet parsed from a string), it returns true and pushes tctxt->style->doc (the stylesheet’s document node) into the result node-set. That inserts a node belonging to the stylesheet’s document tree into a node-set produced while evaluating XPath over the source document, violating the invariant that such a node-set contains only nodes from the evaluation’s own tree. Because the stylesheet document and its nodes are typed and lifetime-managed differently from source nodes, downstream code that consumes the node-set can misinterpret the node — type confusion.

The fix removes the entire idiom-detection branch so a failed URI resolution unconditionally pushes an empty node-set (xmlXPathNewNodeSet(NULL)), which matches the XSLT 1.0 recovery rule and eliminates the stray stylesheet node.

Key insight
The single mistake was testing the already-NULL URI against the stylesheet’s URL on the resolution-failure path, which let the stylesheet’s own document node leak into a source-context node-set; the fix simply returns an empty node-set whenever resolution fails.

Attack Path

  1. Craft an invalid base URI The attacker supplies source XML whose element carries xml:base="http://[", an invalid URI where [ opens an IPv6 literal that has no address and no closing ].
  2. Invoke `document('')` against that node A stylesheet calls document('', /foo), passing the invalid-base node /foo as the second argument so that its base URI drives the resolution.
  3. Force a `NULL` resolution xmlBuildURI resolving the empty string against the invalid base returns NULL, sending execution into the URI == NULL branch of xsltDocumentFunction.
  4. Leak the stylesheet document node Because URI is NULL and a string-parsed stylesheet’s doc->URL is also NULL, the degenerate xmlStrEqual test passes and tctxt->style->doc is pushed into the source-context node-set instead of an empty node-set.
  5. Trigger type confusion Later processing of that node-set treats the stylesheet’s document node as a source-tree node, causing type confusion and potential memory corruption (inferred).

Impact Assessment

An attacker who controls the XML and XSLT fed to an XSLTProcessor transform can cause the stylesheet’s own document node to be injected into a source-tree node-set, producing a type confusion inside libxslt running in the renderer process. The preconditions are that the attacker can run an XSLT transform with a controllable xml:base on the document() second-argument node and a stylesheet whose document URL is NULL (as with parseFromString). The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. A severity rating are not stated in the patch, and any escalation from the type confusion to memory corruption or code execution is (inferred) rather than demonstrated by the diff.

Changed Functions

FunctionChangeNotes
if
third_party/libxslt/chromium/0007-Return-an-empty-node-set-from-document-function-with-invalid-uri.patch
modified
if
third_party/libxslt/src/libxslt/functions.c
modified

Files Changed

  • third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
  • third_party/libxslt/chromium/0007-Return-an-empty-node-set-from-document-function-with-invalid-uri.patch
  • third_party/libxslt/chromium/roll.py
  • third_party/libxslt/src/libxslt/functions.c

Audit Directions

  • Cross-tree node leakage
    Review other libxslt/libxml functions that valuePush nodes onto the XPath stack for cases that can mix stylesheet-tree nodes with source-tree nodes within a single node-set.
  • Degenerate guards on error paths
    Look for conditions that test a variable already known to be NULL or an error sentinel inside its own failure branch (especially xmlStrEqual/pointer checks) guarding node-set or object construction.
  • `xmlBuildURI` failure fallbacks
    Audit callers that branch on a NULL resolution result and fall back to a document-self or default node instead of returning an empty/error result.
From 8005fbd4ba5355dc88df069fd99bf76e3fd2b525 Mon Sep 17 00:00:00 2001
From: Daniel Cheng <[email protected]>
Date: Fri, 31 Jul 2026 20:32:17 -0700
Subject: [PATCH] Return an empty node-set from document('') with an invalid base URI

`document('')` is sometimes used as an idiom for returning a node-set
corresponding to the current stylesheet. However, resolving the empty string
against an invalid base URI produces an invalid URI, and per section
12.1 of the XSLT 1.0 standard:

  If there is an error retrieving the resource, then the XSLT processor
  may signal the error; if it does not signal the error, it must recover
  by returning an empty node-set.

The spec is a bit contradictory here; elsewhere, it also states:

  Note that a zero-length URI reference is a reference to the document
  relative to which the URI reference is being resolved; thus
  document("") refers to the root node of the stylesheet; the tree
  representation of the stylesheet is exactly the same as if the XML
  document containing the stylesheet was the initial source document.

But the standard also says:

  The URI reference may be relative. The base URI of the node in the
  second argument node-set that is first in document order is used as
  the base URI for resolving the relative URI into an absolute URI.

And resolving the empty string against an invalid URI should arguably
produce an invalid URI.

In some ways, this brings Chrome behavior closer to Firefox, though it
still differs in other ways: the specific test case here returns a
different result in Firefox, because Firefox appears to ignore an
attempt to set an invalid base URI, while Chrome does not.

Bug: 495541478
Change-Id: I9ea2be319c1d4295c75d7fa3bf9b177b428cfd65
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8178615
Reviewed-by: Mason Freed <[email protected]>
Reviewed-by: Tom Sepez <[email protected]>
Commit-Queue: Daniel Cheng <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1672234}
---
diff --git a/third_party/libxslt/chromium/0007-Return-an-empty-node-set-from-document-function-with-invalid-uri.patch b/third_party/libxslt/chromium/0007-Return-an-empty-node-set-from-document-function-with-invalid-uri.patch
new file mode 100644
index 0000000..b10b7272
--- /dev/null
+++ b/third_party/libxslt/chromium/0007-Return-an-empty-node-set-from-document-function-with-invalid-uri.patch
@@ -0,0 +1,38 @@
+From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
+From: David Cheng <[email protected]>
+Date: Fri, 31 Jul 2026 15:00:00 -0700
+Subject: [PATCH] Return an empty node-set from document('') with an invalid base URI
+
+`document('')` is sometimes used as an idiom for returning a node-set
+corresponding to the current stylesheet. However, resolving the empty string
+against an invalid base URI produces an invalid URI, and per section
+12.1 of the XSLT 1.0 standard:
+
+  If there is an error retrieving the resource, then the XSLT processor
+  may signal the error; if it does not signal the error, it must recover
+  by returning an empty node-set.
+---
+ libxslt/functions.c | 9 +--------
+ 1 file changed, 1 insertion(+), 8 deletions(-)
+
+diff --git a/libxslt/functions.c b/libxslt/functions.c
+index 11ec039f540cc..317a44e5d01fe 100644
+--- a/libxslt/functions.c
++++ b/libxslt/functions.c
+@@ -337,14 +337,7 @@ xsltDocumentFunction(xmlXPathParserContextPtr ctxt, int nargs)
+         if (base != NULL)
+             xmlFree(base);
+         if (URI == NULL) {
+-            if ((tctxt != NULL) && (tctxt->style != NULL) &&
+-                (tctxt->style->doc != NULL) &&
+-                (xmlStrEqual(URI, tctxt->style->doc->URL))) {
+-                /* This selects the stylesheet's doc itself. */
+-                valuePush(ctxt, xmlXPathNewNodeSet((xmlNodePtr) tctxt->style->doc));
+-            } else {
+-                valuePush(ctxt, xmlXPathNewNodeSet(NULL));
+-            }
++            valuePush(ctxt, xmlXPathNewNodeSet(NULL));
+         } else {
+ 	    xsltDocumentFunctionLoadDocument(ctxt, URI, fragment);
+ 	    xmlFree(URI);
+--
diff --git a/third_party/libxslt/chromium/roll.py b/third_party/libxslt/chromium/roll.py
index 277e0f4..16bf047 100755
--- a/third_party/libxslt/chromium/roll.py
+++ b/third_party/libxslt/chromium/roll.py
@@ -75,6 +75,7 @@
     '0004-Use-a-dedicated-node-type-to-maintain-the-list-of-ca.patch',
     '0005-Verify-dictionary-ownership-before-aliasing-attribut.patch',
     '0006-Fix-type-confusion-in-xsltParseTemplateContent.patch',
+    '0007-Return-an-empty-node-set-from-document-function-with-invalid-uri.patch',
 ]
 
 
diff --git a/third_party/libxslt/src/libxslt/functions.c b/third_party/libxslt/src/libxslt/functions.c
index 11ec039..317a44e 100644
--- a/third_party/libxslt/src/libxslt/functions.c
+++ b/third_party/libxslt/src/libxslt/functions.c
@@ -337,14 +337,7 @@
         if (base != NULL)
             xmlFree(base);
         if (URI == NULL) {
-            if ((tctxt != NULL) && (tctxt->style != NULL) &&
-                (tctxt->style->doc != NULL) &&
-                (xmlStrEqual(URI, tctxt->style->doc->URL))) {
-                /* This selects the stylesheet's doc itself. */
-                valuePush(ctxt, xmlXPathNewNodeSet((xmlNodePtr) tctxt->style->doc));
-            } else {
-                valuePush(ctxt, xmlXPathNewNodeSet(NULL));
-            }
+            valuePush(ctxt, xmlXPathNewNodeSet(NULL));
         } else {
 	    xsltDocumentFunctionLoadDocument(ctxt, URI, fragment);
 	    xmlFree(URI);
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js b/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
index a01f3b3..99fab7ab 100644
--- a/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
@@ -41,3 +41,26 @@
   assert_true(Array.prototype.every.call(resultDoc.documentElement.children,
                                          (e) => e.localName == "success"));
 }, `xsl:document function disabled in transformToDocument`);
+
+test(() => {
+  // "http://[" is an invalid URI; [ signals the start of an IPv6 literal, but
+  // there is no actual address, nor is there a closing ].
+  const xmlWithInvalidBase = parser.parseFromString(
+      `<foo xml:base="http://[" />`, "application/xml");
+  const xsltStringWithInvalidBaseNode = `
+  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+    <xsl:template match="/">
+      <result>
+        <count><xsl:value-of select="count(document('', /foo))" /></count>
+      </result>
+    </xsl:template>
+  </xsl:stylesheet>
+  `;
+  const xsltDocWithInvalidBaseNode = parser.parseFromString(
+      xsltStringWithInvalidBaseNode, "application/xml");
+  const processor = new XSLTProcessor();
+  processor.importStylesheet(xsltDocWithInvalidBaseNode);
+  const resultDoc = processor.transformToDocument(xmlWithInvalidBase);
+  assert_equals(resultDoc.querySelector("count").textContent, "0");
+}, `document() with invalid xml:base target node returns an empty node-set`);
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js b/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
index a01f3b3..99fab7ab 100644
--- a/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
+++ b/third_party/blink/web_tests/external/wpt/xml/xslt/document-function.window.js
@@ -41,3 +41,26 @@
   assert_true(Array.prototype.every.call(resultDoc.documentElement.children,
                                          (e) => e.localName == "success"));
 }, `xsl:document function disabled in transformToDocument`);
+
+test(() => {
+  // "http://[" is an invalid URI; [ signals the start of an IPv6 literal, but
+  // there is no actual address, nor is there a closing ].
+  const xmlWithInvalidBase = parser.parseFromString(
+      `<foo xml:base="http://[" />`, "application/xml");
+  const xsltStringWithInvalidBaseNode = `
+  <xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+    <xsl:template match="/">
+      <result>
+        <count><xsl:value-of select="count(document('', /foo))" /></count>
+      </result>
+    </xsl:template>
+  </xsl:stylesheet>
+  `;
+  const xsltDocWithInvalidBaseNode = parser.parseFromString(
+      xsltStringWithInvalidBaseNode, "application/xml");
+  const processor = new XSLTProcessor();
+  processor.importStylesheet(xsltDocWithInvalidBaseNode);
+  const resultDoc = processor.transformToDocument(xmlWithInvalidBase);
+  assert_equals(resultDoc.querySelector("count").textContent, "0");
+}, `document() with invalid xml:base target node returns an empty node-set`);
+
Loading diff…

Original Bug Report

reported by [email protected]

Type Confusion in libxslt xsltDocumentFunction via stylesheet document exposure

Flapjack (go/flapjack), an LLM-powered static analysis tool, has identified the following potential security issue.

Overview: A logic error in xsltDocumentFunction allows an attacker to obtain a reference to the active stylesheet document. By applying generate-id() to uncompiled instruction nodes in this document, internal psvi fields are overwritten with integer IDs, leading to a potential type confusion and arbitrary code execution during transformation.

Affected files:

  • third_party/libxslt/src/libxslt/functions.c

Estimated timestamp from git blame: 2015-06-23

Summary

A potential type confusion vulnerability exists in libxslt’s document() function. When URI resolution fails, the function can incorrectly return the active stylesheet document itself instead of an empty node-set. This exposure allows an attacker to manipulate the document’s internal metadata using the generate-id() function, leading to type confusion when the stylesheet is later processed.

Technical Details

In third_party/libxslt/src/libxslt/functions.c, the xsltDocumentFunction handles the XSLT document() call. The function attempts to resolve the URI for the document using xmlBuildURI:

336:         URI = xmlBuildURI(url, base);
337:         if (base != NULL)
338:             xmlFree(base);
339:         if (URI == NULL) {
340:             if ((tctxt != NULL) && (tctxt->style != NULL) &&
341:                 (tctxt->style->doc != NULL) &&
342:                 (xmlStrEqual(URI, tctxt->style->doc->URL))) {
343:                 /* This selects the stylesheet's doc itself. */
344:                 valuePush(ctxt, xmlXPathNewNodeSet((xmlNodePtr) tctxt->style->doc));
345:             } else {
346:                 valuePush(ctxt, xmlXPathNewNodeSet(NULL));
347:             }
348:         } else {
349:             xsltDocumentFunctionLoadDocument(ctxt, URI, fragment);
350:             xmlFree(URI);
351:         }

If xmlBuildURI returns NULL (which happens if base is an invalid URI like invalid://::), the code checks if URI is equal to the stylesheet document’s URL. If the stylesheet was loaded from memory (e.g., via XSLTProcessor.importStylesheet()), tctxt->style->doc->URL is NULL. Since xmlStrEqual(NULL, NULL) returns 1, the function pushes the live stylesheet document onto the XPath stack.

This short-circuit logic bypasses a previous fix (issue 413080347) in xsltDocumentFunctionLoadDocument, which securely copies the document and strips internal psvi pointers.

Once the live stylesheet document is returned, an attacker can use XPath to select an uncompiled XSLT instruction node (like <xsl:message>) whose psvi field is still NULL. Calling the generate-id() function on this node in xsltGenerateIdFunction assigns it a new ID by incrementing tctxt->currentId and storing this integer directly into the psvi field:

796:         id = ++tctxt->currentId;
797:         *psviPtr = (void *) (size_t) id;

When the XSLT engine subsequently evaluates this instruction in xsltApplySequenceConstructor (transform.c:2736), it expects the psvi field to be a valid pointer to an xsltStylePreComp structure:

2736:             xsltStylePreCompPtr info = (xsltStylePreCompPtr) cur->psvi;
...
2756:             if (info->func != NULL) {
...
2761:                 info->func(ctxt, contextNode, cur, (xsltElemPreCompPtr) info);

Because info is now an attacker-controlled integer (the generated ID), dereferencing it to call info->func results in a type confusion. An attacker could potentially spray the heap and increment currentId (by calling generate-id() repeatedly) to point to a controlled fake xsltStylePreComp object, leading to Remote Code Execution in the renderer process.

Suggested Steps to Reproduce (Theoretical)

  1. Load a malicious XSLT stylesheet from a memory buffer (e.g., using XSLTProcessor.importStylesheet()).
  2. In the XML input, include a node with an invalid base URI: <foo xml:base="invalid://::"/>.
  3. In the stylesheet, call document('', $foo_node). This forces xmlBuildURI to return NULL and exposes the active stylesheet document.
  4. Select an uncompiled instruction node (e.g., <xsl:message>) from the returned document using XPath.
  5. Call generate-id() on dummy nodes to increment currentId to a predictable heap address where a fake xsltStylePreComp object is sprayed.
  6. Call generate-id() on the uncompiled instruction node to overwrite its psvi field with this target address.
  7. Trigger the execution of the corrupted instruction node in the stylesheet.
  8. The engine dereferences the fake object and calls the attacker-controlled function pointer.

Note: These are suggested steps, as this AI agent does not yet have the ability to run code to verify the exploit end-to-end.

Suggested Fix

In xsltDocumentFunction, remove the short-circuit logic that directly pushes tctxt->style->doc. Instead, always rely on xsltDocumentFunctionLoadDocument, which handles the safe copying of the stylesheet document and stripping of psvi fields. Alternatively, explicitly check that URI and tctxt->style->doc->URL are not NULL before calling xmlStrEqual.

Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f


Results from Flapjack so far have been promising, but it can be wrong in its deductions. At this time, it does not produce proof of concepts or fuzzer tests. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve Flapjack’s accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker