Commit 9073875eb98 for php.net
commit 9073875eb98416595f1666df0f6a728bab2bc5c7
Author: David Carlier <devnexen@gmail.com>
Date: Mon Jul 6 23:25:57 2026 +0100
ext/dom: Use-after-free with namespace nodes from XSLTProcessor::registerPHPFunctions().
Fix #22621
Namespace parents from an external document() were wrapped directly
instead of through the proxy factory, so they skipped the copy that
keeps non-main-document nodes alive. A DOMNameSpaceNode retained by
userland then dangled once xsltFreeTransformContext() freed the external
document. Route the namespace parent through the proxy factory too.
close GH-22623
diff --git a/NEWS b/NEWS
index cd259f06660..f40c6dfd8da 100644
--- a/NEWS
+++ b/NEWS
@@ -33,6 +33,8 @@ PHP NEWS
. Fixed Dom\DtdNamedNodeMap integer dimension access so negative indexes
return NULL and indexes outside the int range throw ValueError instead of
returning the first entity or notation. (Weilin Du)
+ . Fixed bug GH-22623 (use after free with namespace nodes from
+ XSLTProcessor::registerFunctions())/ (David Carlier)
- Exif:
. Fixed bug GH-11020 (exif_read_data() emits a spurious "Illegal IFD size"
diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c
index 622dd187dd8..349c304c9f2 100644
--- a/ext/dom/xpath_callbacks.c
+++ b/ext/dom/xpath_callbacks.c
@@ -347,15 +347,15 @@ static zval *php_dom_xpath_callback_fetch_args(xmlXPathParserContextPtr ctxt, ui
xmlNodePtr node = obj->nodesetval->nodeTab[j];
zval child;
if (UNEXPECTED(node->type == XML_NAMESPACE_DECL)) {
- xmlNodePtr nsparent = node->_private;
xmlNsPtr original = (xmlNsPtr) node;
/* Make sure parent dom object exists, so we can take an extra reference. */
zval parent_zval; /* don't destroy me, my lifetime is transferred to the fake namespace decl */
- php_dom_create_object(nsparent, &parent_zval, intern);
+ proxy_factory(node->_private, &parent_zval, intern, ctxt);
dom_object *parent_intern = Z_DOMOBJ_P(&parent_zval);
+ xmlNodePtr parent = dom_object_get_node(parent_intern);
- php_dom_create_fake_namespace_decl(nsparent, original, &child, parent_intern);
+ php_dom_create_fake_namespace_decl(parent, original, &child, parent_intern);
} else {
proxy_factory(node, &child, intern, ctxt);
}
diff --git a/ext/xsl/tests/gh22621.phpt b/ext/xsl/tests/gh22621.phpt
new file mode 100644
index 00000000000..b31671eff63
--- /dev/null
+++ b/ext/xsl/tests/gh22621.phpt
@@ -0,0 +1,63 @@
+--TEST--
+GH-22621 (UAF via XSLTProcessor::registerPHPFunctions() retaining namespace nodes from an external document)
+--EXTENSIONS--
+dom
+xsl
+--CREDITS--
+ExPatch-LLC
+--FILE--
+<?php
+$cDIR = __DIR__;
+file_put_contents($cDIR . '/gh22621_external.xml', '<root xmlns:custom="urn:custom"><data>secret</data></root>');
+$uri = 'file:///' . ltrim(str_replace('\\', '/', $cDIR), '/') . '/gh22621_external.xml';
+
+$stored_ns = null;
+
+function capture_ns($nodes) {
+ global $stored_ns;
+ foreach ($nodes as $node) {
+ if ($node instanceof DOMNameSpaceNode) {
+ $stored_ns = $node;
+ }
+ }
+ return "captured";
+}
+
+$xsl = new DOMDocument();
+$xsl->loadXML(<<<XSL
+<?xml version="1.0"?>
+<xsl:stylesheet version="1.0"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
+ xmlns:php="http://php.net/xsl">
+ <xsl:template match="/">
+ <result>
+ <xsl:value-of select="php:function('capture_ns', document('$uri')/*/namespace::*)"/>
+ </result>
+ </xsl:template>
+</xsl:stylesheet>
+XSL);
+
+$doc = new DOMDocument();
+$doc->loadXML('<input/>');
+
+$proc = new XSLTProcessor();
+$proc->registerPHPFunctions();
+$proc->importStylesheet($xsl);
+echo $proc->transformToXml($doc);
+
+var_dump($stored_ns->localName);
+var_dump($stored_ns->namespaceURI);
+var_dump($stored_ns->parentNode->localName);
+var_dump($stored_ns->ownerDocument instanceof DOMDocument);
+?>
+--CLEAN--
+<?php
+@unlink(__DIR__ . '/gh22621_external.xml');
+?>
+--EXPECTF--
+<?xml version="1.0"?>
+<result xmlns:php="http://php.net/xsl">captured</result>
+string(6) "custom"
+string(10) "urn:custom"
+string(4) "root"
+bool(true)