From 6abce879c9f50db6197a4cad868d78c53c832b0c Mon Sep 17 00:00:00 2001 From: David Carlier Date: Mon, 6 Jul 2026 23:25:57 +0100 Subject: [PATCH] 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. --- ext/dom/xpath_callbacks.c | 6 ++-- ext/xsl/tests/gh22621.phpt | 63 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 ext/xsl/tests/gh22621.phpt diff --git a/ext/dom/xpath_callbacks.c b/ext/dom/xpath_callbacks.c index 622dd187dd84..349c304c9f25 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 000000000000..b31671eff630 --- /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-- +secret'); +$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); + +$doc = new DOMDocument(); +$doc->loadXML(''); + +$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-- + +--EXPECTF-- + +captured +string(6) "custom" +string(10) "urn:custom" +string(4) "root" +bool(true)