Medium watch CVSS 8.8 chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in XML
DescriptionInappropriate implementation in XML
ComponentXML
Bug ClassLogic Error
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker513049042
Fix commit7d8b7fc6c403 (chromium/src) +79/-0
CWECWE-841
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-05-14 (per vendor advisory)
Fix landed2026-08-27 (public git)
Disclosed2026-09-22
Patch gap26 days — fix public in source before the advisory

Background

`libxslt`
The C library Blink links for XSLT 1.0 transforms, driven through Blink’s XSLTProcessor and XSLStyleSheet wrappers.
`xsltInit()`
A libxslt global-initialization routine that auto-registers built-in and non-standard extension modules, and which libxslt otherwise 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 libxslt API that removes a previously registered extension element so that standard xsl:fallback handling 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.

Key insight
The single mistake was letting 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

  1. Supply a hostile stylesheet Web content drives XSLTProcessor.importStylesheet() with an attacker-authored XSLT document, reaching XSLStyleSheet::CompileStyleSheet().
  2. Implicit init registers extensions Compilation triggers libxslt’s implicit xsltInit(), registering saxon:output, xalan:write, xt:document, xsl:document, and libxslt:debug and binding them at compile time.
  3. 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), which element-available() reports as available, causing libxslt to run the extension’s document-output path instead of standard xsl:fallback.
  4. 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

An attacker who can run an XSLT transform in the renderer gains access to non-standard 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

FunctionChangeNotes
if
third_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.txt
  • third_party/blink/renderer/core/xml/xslt_extensions.cc
  • third_party/blink/renderer/core/xml/xslt_extensions.h
  • third_party/blink/renderer/core/xml/xsl_style_sheet_libxslt.cc
  • third_party/blink/web_tests/virtual/xslt-enabled/fast/xsl/xslt/xslt-extension-elements.html

Audit Directions

  • Implicit third-party library initialization
    Audit 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 surface
    Enumerate all extension modules, elements, and functions libxslt registers by default and confirm each is either intended or explicitly unregistered, mirroring the xsltUnregisterExtModuleElement() sweep here.
  • Contract-vs-implementation gaps
    Look 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.
From 7d8b7fc6c403d128ae37b5ed0d25fd3c95ff4e33 Mon Sep 17 00:00:00 2001
From: Emmanuele Bassi <[email protected]>
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 <[email protected]>
Commit-Queue: Dominik Röttsches <[email protected]>
Reviewed-by: Dominik Röttsches <[email protected]>
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>
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.