Commit 3221a076f26 for php.net

commit 3221a076f26da584b30ed90ce4a46fbb7343069b
Author: David Carlier <devnexen@gmail.com>
Date:   Thu Jun 25 13:12:17 2026 +0100

    ext/dom: fix UAF when setting an attribute colliding by local name.

    Fix #22447

    xmlHasProp() matches an attribute by local name only, ignoring its
    namespace, whereas xmlAddChild()/xmlAddPrevSibling() dedup an incoming
    no-namespace attribute via xmlHasNsProp(..., NULL), which matches only
    attributes with no namespace. When both a no-namespace and a namespaced
    attribute share a local name, the pre-insertion check unlinked the wrong
    (namespaced) attribute, leaving libxml to free the still-wrapped
    no-namespace duplicate and producing a use-after-free at request
    shutdown.

    Use xmlHasNsProp(..., NULL) so the pre-insertion check matches libxml's
    internal duplicate detection, as advised by @nwellnhof.

    Close GH-22452

diff --git a/NEWS b/NEWS
index cfb82c4f0ca..6b42bbf1cbd 100644
--- a/NEWS
+++ b/NEWS
@@ -8,6 +8,9 @@ PHP                                                                        NEWS
 - DOM:
   . Fixed bug GH-22825 (DOMElement::setAttribute() fails silently when the DTD
     declares a default value for the attribute). (iliaal)
+  . Fixed bug GH-22447 (UAF at dom_objects_free_storage when setting an
+    attribute node that collides by local name with a namespaced
+    attribute). (David Carlier)

 - Sockets:
   . Fixed various memory related issues in ext/sockets. (David Carlier)
diff --git a/ext/dom/element.c b/ext/dom/element.c
index b2564428db9..3bce1bdac2a 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -726,6 +726,8 @@ static void dom_element_set_attribute_node_common(INTERNAL_FUNCTION_PARAMETERS,
 	nsp = attrp->ns;
 	if (use_ns && nsp != NULL) {
 		existattrp = xmlHasNsProp(nodep, attrp->name, nsp->href);
+	} else if (nsp == NULL) {
+		existattrp = xmlHasNsProp(nodep, attrp->name, NULL);
 	} else {
 		existattrp = xmlHasProp(nodep, attrp->name);
 	}
diff --git a/ext/dom/node.c b/ext/dom/node.c
index df806bddfae..81c80cb0c8a 100644
--- a/ext/dom/node.c
+++ b/ext/dom/node.c
@@ -965,7 +965,7 @@ static void dom_node_insert_before_legacy(zval *return_value, zval *ref, dom_obj
 			xmlAttrPtr lastattr;

 			if (child->ns == NULL)
-				lastattr = xmlHasProp(refp->parent, child->name);
+				lastattr = xmlHasNsProp(refp->parent, child->name, NULL);
 			else
 				lastattr = xmlHasNsProp(refp->parent, child->name, child->ns->href);
 			if (lastattr != NULL && lastattr->type != XML_ATTRIBUTE_DECL) {
@@ -1012,7 +1012,7 @@ static void dom_node_insert_before_legacy(zval *return_value, zval *ref, dom_obj
 			xmlAttrPtr lastattr;

 			if (child->ns == NULL)
-				lastattr = xmlHasProp(parentp, child->name);
+				lastattr = xmlHasNsProp(parentp, child->name, NULL);
 			else
 				lastattr = xmlHasNsProp(parentp, child->name, child->ns->href);
 			if (lastattr != NULL && lastattr->type != XML_ATTRIBUTE_DECL) {
@@ -1374,7 +1374,7 @@ static void dom_node_append_child_legacy(zval *return_value, dom_object *intern,
 		xmlAttrPtr lastattr;

 		if (child->ns == NULL)
-			lastattr = xmlHasProp(nodep, child->name);
+			lastattr = xmlHasNsProp(nodep, child->name, NULL);
 		else
 			lastattr = xmlHasNsProp(nodep, child->name, child->ns->href);
 		if (lastattr != NULL && lastattr->type != XML_ATTRIBUTE_DECL) {
diff --git a/ext/dom/tests/gh22447.phpt b/ext/dom/tests/gh22447.phpt
new file mode 100644
index 00000000000..396a0ff1ee1
--- /dev/null
+++ b/ext/dom/tests/gh22447.phpt
@@ -0,0 +1,25 @@
+--TEST--
+GH-22447 (UAF at dom_objects_free_storage when setAttributeNode collides with a namespaced attribute of the same local name)
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+$dom = Dom\HTMLDocument::createEmpty();
+
+$attribute1 = $dom->createAttribute("my-attribute");
+$container  = $dom->appendChild($dom->createElement("container"));
+$attribute2 = $dom->createAttribute("my-attribute");
+$attribute4 = $dom->createAttributeNS("urn:a", "my-attribute");
+
+$container->setAttributeNode($attribute1);
+$container->setAttributeNode($attribute4);
+
+var_dump($container->setAttributeNode($attribute2) === $attribute1);
+var_dump($container->setAttributeNode($attribute1) === $attribute2);
+
+echo $dom->saveXml($container), PHP_EOL;
+?>
+--EXPECT--
+bool(true)
+bool(true)
+<container xmlns="http://www.w3.org/1999/xhtml" xmlns:ns1="urn:a" ns1:my-attribute="" my-attribute=""></container>