CVE-2026-95369
Overview
Background
- `libxslt`
- The C library Blink links for XSLT 1.0 transforms, driven through Blink’s
XSLTProcessorandXSLStyleSheetwrappers. - `xsltInit()`
- A
libxsltglobal-initialization routine that auto-registers built-in and non-standard extension modules, and whichlibxsltotherwise calls implicitly on first use. - Extension elements
- Namespaced elements (e.g.
saxon:output,xalan:write,xt:document,xsl:document,libxslt:debug) that a stylesheet can invoke, several of which implement multi-document output by writing additional result trees to caller-supplied URIs. - `xsltUnregisterExtModuleElement()`
- The
libxsltAPI that removes a previously registered extension element so that standardxsl:fallbackhandling applies instead of the extension’s behavior.
Root Cause Analysis
When XSLStyleSheet::CompileStyleSheet() compiled a stylesheet, libxslt would implicitly run xsltInit(), which registers a set of non-standard multi-document output extension elements (libxslt:debug, saxon:output, xalan:write, xt:document, and xsl:document) that bind to xsltDocumentElem at compile time, before XSLTProcessor applies its per-transform configuration. Blink’s XSLTProcessor is documented to produce only a single result tree, so these auxiliary output elements were never meant to be reachable, yet a page-supplied stylesheet could name them and have libxslt route output through the extension rather than through standard fallback handling. The invariant violated is that the transform surface exposed to web content should match XSLTProcessor’s single-result-tree contract; instead the implicit initialization silently widened it.
The fix hoists initialization into a dedicated InitializeLibXSLT() that runs xsltInit() once and then calls xsltUnregisterExtModuleElement() for each of the five extension elements before any stylesheet is compiled. Because CompileStyleSheet() now calls InitializeLibXSLT() first and any later xsltInit() is idempotent, the extensions are guaranteed unregistered when compilation binds elements. The added web test confirms element-available() reports these elements as unavailable and that xsl:fallback applies.
libxslt implicitly initialize itself and thereby register non-standard multi-document output extension elements that fall outside XSLTProcessor’s single-result-tree contract; the fix takes control of initialization order via InitializeLibXSLT() and unregisters those elements before any stylesheet compilation can bind them.Attack Path
- Supply a hostile stylesheet
Web content drives
XSLTProcessor.importStylesheet()with an attacker-authored XSLT document, reachingXSLStyleSheet::CompileStyleSheet(). - Implicit init registers extensions
Compilation triggers
libxslt’s implicitxsltInit(), registeringsaxon:output,xalan:write,xt:document,xsl:document, andlibxslt:debugand binding them at compile time. - Invoke a multi-document output element
The stylesheet declares one of the extension namespaces and uses the corresponding element (e.g.
xalan:write/xsl:document), whichelement-available()reports as available, causinglibxsltto run the extension’s document-output path instead of standardxsl:fallback. - Trigger unintended output behavior
The multi-document output handler produces an additional result tree directed to a stylesheet-controlled URI, behavior outside
XSLTProcessor’s intended single-result-tree surface.(inferred)— the patch does not include a concrete out-of-bounds sink, file write, or memory-corruption primitive reachable through this path.
Impact Assessment
libxslt multi-document output extension elements that Blink’s XSLTProcessor never intended to expose, escaping its single-result-tree contract; the only precondition is the ability to import and run a crafted stylesheet, which ordinary web content can do. The concrete consequence is that the transform behaves according to the extension rather than standard xsl:fallback semantics. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.8. The advisory states only “Inappropriate implementation in XML” (a LogicError) with no exploited-in-the-wild or severity claim provided here.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/xml/xslt_extensions.cc |
modified |
Files Changed
third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txtthird_party/blink/renderer/core/xml/xslt_extensions.ccthird_party/blink/renderer/core/xml/xslt_extensions.hthird_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.ccthird_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html
Audit Directions
- Implicit third-party library initializationAudit other places where Blink relies on
libxml/libxslt(or similar C libraries) to self-initialize, since implicit init can register capabilities the wrapper never intended to expose. - Extension/registration surfaceEnumerate all extension modules, elements, and functions
libxsltregisters by default and confirm each is either intended or explicitly unregistered, mirroring thexsltUnregisterExtModuleElement()sweep here. - Contract-vs-implementation gapsLook for features whose documented contract (e.g.
XSLTProcessor’s single result tree) is narrower than the underlying library’s behavior, where unbounded library defaults silently widen the web-exposed surface.
Patch
From 7d8b7fc6c403d128ae37b5ed0d25fd3c95ff4e33 Mon Sep 17 00:00:00 2001
From: Emmanuele Bassi <ebassi@igalia.com>
Date: Thu, 27 Aug 2026 02:15:53 -0700
Subject: [PATCH] Unregister libxslt extra extensions
During initialization, libxslt registers various additional non-standard
extension modules. These modules are bound to xsltDocumentElem at
stylesheet compile time, before the per-transform configuration that
XSLTProcessor applies.
Instead of relying on libxslt implicitly calling xsltInit() for us, we
hoist it up inside our own initialization function, and undo the
registration of the extensions. Any additional call to xsltInit() is
idempotent to the first one.
Bug: 513049042
Change-Id: I15bbe5042ca49d1d7612776a6300b05a15e8aeb6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8034081
Reviewed-by: Rune Lillesveen <futhark@chromium.org>
Commit-Queue: Dominik Röttsches <drott@chromium.org>
Reviewed-by: Dominik Röttsches <drott@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1687063}
---
diff --git a/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc b/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
index 8b75ea4a..9a5a4da 100644
--- a/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
+++ b/third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
@@ -33,6 +33,7 @@
#include "third_party/blink/renderer/core/xml/parser/xml_document_parser_scope.h"
#include "third_party/blink/renderer/core/xml/parser/xml_parser_input.h"
#include "third_party/blink/renderer/core/xml/xsl_style_sheet.h"
+#include "third_party/blink/renderer/core/xml/xslt_extensions.h"
#include "third_party/blink/renderer/core/xml/xslt_processor.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_initiator_type_names.h"
#include "third_party/blink/renderer/platform/loader/fetch/fetch_parameters.h"
@@ -271,6 +272,8 @@
}
xsltStylesheetPtr XSLStyleSheet::CompileStyleSheet() {
+ InitializeLibXSLT();
+
// FIXME: Hook up error reporting for the stylesheet compilation process.
if (embedded_)
return xsltLoadStylesheetPI(GetDocument());
diff --git a/third_party/blink/renderer/core/xml/xslt_extensions.cc b/third_party/blink/renderer/core/xml/xslt_extensions.cc
index ef34cc6..46caa3f 100644
--- a/third_party/blink/renderer/core/xml/xslt_extensions.cc
+++ b/third_party/blink/renderer/core/xml/xslt_extensions.cc
@@ -29,6 +29,7 @@
#include <libxml/xpathInternals.h>
#include <libxslt/extensions.h>
#include <libxslt/extra.h>
+#include <libxslt/xslt.h>
#include <libxslt/xsltutils.h>
#include "base/check.h"
@@ -84,4 +85,26 @@
ExsltNodeSetFunction);
}
+void InitializeLibXSLT() {
+ static bool did_init = false;
+ if (did_init) {
+ return;
+ }
+ did_init = true;
+
+ // xsltInit() auto-registers a set of non-standard multi-document output
+ // extension elements (libxslt:debug, saxon:output, xalan:write,
+ // xt:document and xsl:document). XSLTProcessor only ever produces a
+ // single result tree, so unregister them before any stylesheet is
+ // compiled so that the standard xsl:fallback handling applies instead.
+ xsltInit();
+ xsltUnregisterExtModuleElement((const xmlChar*)"debug",
+ XSLT_LIBXSLT_NAMESPACE);
+ xsltUnregisterExtModuleElement((const xmlChar*)"output",
+ XSLT_SAXON_NAMESPACE);
+ xsltUnregisterExtModuleElement((const xmlChar*)"write", XSLT_XALAN_NAMESPACE);
+ xsltUnregisterExtModuleElement((const xmlChar*)"document", XSLT_XT_NAMESPACE);
+ xsltUnregisterExtModuleElement((const xmlChar*)"document", XSLT_NAMESPACE);
+}
+
} // namespace blink
diff --git a/third_party/blink/renderer/core/xml/xslt_extensions.h b/third_party/blink/renderer/core/xml/xslt_extensions.h
index 9e48fd13..a99f396 100644
--- a/third_party/blink/renderer/core/xml/xslt_extensions.h
+++ b/third_party/blink/renderer/core/xml/xslt_extensions.h
@@ -34,6 +34,7 @@
void RegisterXSLTExtensions(xsltTransformContextPtr ctxt,
const ExecutionContext* context);
+void InitializeLibXSLT();
}
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_XML_XSLT_EXTENSIONS_H_
diff --git a/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txt b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txt
new file mode 100644
index 0000000..4630976
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txt
@@ -0,0 +1,4 @@
+CONSOLE WARNING: XSLTProcessor and XSLT Processing Instructions have been deprecated by all browsers. These features will be removed from this browser soon. See https://chromestatus.com/feature/4709671889534976.
+This is a testharness.js-based test.
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html
new file mode 100644
index 0000000..618a50e
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<script src="../../../../../resources/testharness.js"></script>
+<script src="../../../../../resources/testharnessreport.js"></script>
+<script>
+function transform(xslSource) {
+ const parser = new DOMParser();
+ const xsl = parser.parseFromString(xslSource, 'text/xml');
+ const xml = parser.parseFromString('<root/>', 'text/xml');
+ const processor = new XSLTProcessor();
+ processor.importStylesheet(xsl);
+ return processor.transformToFragment(xml, document);
+}
+
+test(() => {
+ const result = transform(`<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:libxslt="http://xmlsoft.org/XSLT/namespace"
+ xmlns:saxon="http://icl.com/saxon"
+ xmlns:xalan="org.apache.xalan.xslt.extensions.Redirect"
+ xmlns:xt="http://www.jclark.com/xt">
+ <xsl:output method="text"/>
+ <xsl:template match="/"
+ >[<xsl:if test="element-available('libxslt:debug')">libxslt:debug </xsl:if
+ ><xsl:if test="element-available('saxon:output')">saxon:output </xsl:if
+ ><xsl:if test="element-available('xalan:write')">xalan:write </xsl:if
+ ><xsl:if test="element-available('xt:document')">xt:document </xsl:if
+ ><xsl:if test="element-available('xsl:document')">xsl:document </xsl:if
+ >]</xsl:template>
+ </xsl:stylesheet>`);
+ assert_equals(result.textContent, '[]');
+}, 'non-standard multi-document output extension elements are not available');
+
+test(() => {
+ const result = transform(`<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xalan="org.apache.xalan.xslt.extensions.Redirect"
+ extension-element-prefixes="xalan">
+ <xsl:output method="text"/>
+ <xsl:template match="/">
+ <xalan:write select="{name(*)}">
+ <xsl:fallback>fallback</xsl:fallback>
+ </xalan:write>
+ </xsl:template>
+ </xsl:stylesheet>`);
+ assert_not_equals(result, null, 'transform should succeed');
+ assert_equals(result.textContent.trim(), 'fallback');
+}, 'unsupported extension elements run their xsl:fallback content');
+</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txt b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txt
new file mode 100644
index 0000000..4630976
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements-expected.txt
@@ -0,0 +1,4 @@
+CONSOLE WARNING: XSLTProcessor and XSLT Processing Instructions have been deprecated by all browsers. These features will be removed from this browser soon. See https://chromestatus.com/feature/4709671889534976.
+This is a testharness.js-based test.
+Harness: the test ran to completion.
+
diff --git a/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html
new file mode 100644
index 0000000..618a50e
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<script src="../../../../../resources/testharness.js"></script>
+<script src="../../../../../resources/testharnessreport.js"></script>
+<script>
+function transform(xslSource) {
+ const parser = new DOMParser();
+ const xsl = parser.parseFromString(xslSource, 'text/xml');
+ const xml = parser.parseFromString('<root/>', 'text/xml');
+ const processor = new XSLTProcessor();
+ processor.importStylesheet(xsl);
+ return processor.transformToFragment(xml, document);
+}
+
+test(() => {
+ const result = transform(`<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:libxslt="http://xmlsoft.org/XSLT/namespace"
+ xmlns:saxon="http://icl.com/saxon"
+ xmlns:xalan="org.apache.xalan.xslt.extensions.Redirect"
+ xmlns:xt="http://www.jclark.com/xt">
+ <xsl:output method="text"/>
+ <xsl:template match="/"
+ >[<xsl:if test="element-available('libxslt:debug')">libxslt:debug </xsl:if
+ ><xsl:if test="element-available('saxon:output')">saxon:output </xsl:if
+ ><xsl:if test="element-available('xalan:write')">xalan:write </xsl:if
+ ><xsl:if test="element-available('xt:document')">xt:document </xsl:if
+ ><xsl:if test="element-available('xsl:document')">xsl:document </xsl:if
+ >]</xsl:template>
+ </xsl:stylesheet>`);
+ assert_equals(result.textContent, '[]');
+}, 'non-standard multi-document output extension elements are not available');
+
+test(() => {
+ const result = transform(`<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:xalan="org.apache.xalan.xslt.extensions.Redirect"
+ extension-element-prefixes="xalan">
+ <xsl:output method="text"/>
+ <xsl:template match="/">
+ <xalan:write select="{name(*)}">
+ <xsl:fallback>fallback</xsl:fallback>
+ </xalan:write>
+ </xsl:template>
+ </xsl:stylesheet>`);
+ assert_not_equals(result, null, 'transform should succeed');
+ assert_equals(result.textContent.trim(), 'fallback');
+}, 'unsupported extension elements run their xsl:fallback content');
+</script>