Commit 0dd7f31f151 for php.net
commit 0dd7f31f151636ecab5ef668087693ae266895ce
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Mon Aug 17 19:56:11 2026 -0400
Fix UAF when setAttributeNS() frees a wrapped attribute child
dom_set_attribute_ns_modern() handed the element straight to
xmlSetNsProp(), which frees the existing attribute's child list, so a
live Dom\Text wrapper for one of those children was left pointing at
freed memory. Unlink the wrapped children first, as
dom_set_attribute_ns_legacy() already does.
Closes GH-23348
diff --git a/NEWS b/NEWS
index 182c80517b0..d9f2e25bcdf 100644
--- a/NEWS
+++ b/NEWS
@@ -11,6 +11,8 @@ PHP NEWS
DOMDocument::xinclude(). (iliaal)
. Fixed bug GH-23331 (UAF when node_list_unlink() skips attribute children
that still have a live wrapper). (iliaal)
+ . Fixed a use-after-free when Dom\Element::setAttributeNS() replaces the
+ value of an attribute whose child still has a live wrapper. (iliaal)
- Intl:
. Fixed a double-free when IntlGregorianCalendar construction fails after
diff --git a/ext/dom/element.c b/ext/dom/element.c
index 3bce1bdac2a..8af5d54db33 100644
--- a/ext/dom/element.c
+++ b/ext/dom/element.c
@@ -1030,6 +1030,10 @@ static void dom_set_attribute_ns_modern(dom_object *intern, xmlNodePtr elemp, ze
if (errorcode == 0) {
php_dom_libxml_ns_mapper *ns_mapper = php_dom_get_ns_mapper(intern);
xmlNsPtr ns = php_dom_libxml_ns_mapper_get_ns_raw_prefix_string(ns_mapper, prefix, xmlStrlen(prefix), uri);
+ xmlNodePtr existing = (xmlNodePtr) xmlHasNsProp(elemp, localname, ns == NULL ? NULL : ns->href);
+ if (existing != NULL && existing->type != XML_ATTRIBUTE_DECL) {
+ node_list_unlink(existing->children);
+ }
xmlAttrPtr attr = xmlSetNsProp(elemp, ns, localname, BAD_CAST value);
if (UNEXPECTED(attr == NULL)) {
php_dom_throw_error(INVALID_STATE_ERR, /* strict */ true);
diff --git a/ext/dom/tests/modern/common/Element_setAttributeNS_live_child.phpt b/ext/dom/tests/modern/common/Element_setAttributeNS_live_child.phpt
new file mode 100644
index 00000000000..22475c33cf8
--- /dev/null
+++ b/ext/dom/tests/modern/common/Element_setAttributeNS_live_child.phpt
@@ -0,0 +1,37 @@
+--TEST--
+setAttributeNS() keeps an attribute child that still has a live wrapper
+--EXTENSIONS--
+dom
+--FILE--
+<?php
+
+$doc = Dom\XMLDocument::createFromString('<root xmlns:p="urn:x" p:attr="old"/>');
+$el = $doc->documentElement;
+$text = $el->getAttributeNodeNS('urn:x', 'attr')->firstChild;
+$el->setAttributeNS('urn:x', 'p:attr', 'new');
+echo "prefixed, detached: ";
+var_dump($text->parentNode === null);
+echo "prefixed, text: ";
+var_dump($text->textContent);
+echo "prefixed, new value: ";
+var_dump($el->getAttributeNS('urn:x', 'attr'));
+
+$doc = Dom\XMLDocument::createFromString('<root attr="old"/>');
+$el = $doc->documentElement;
+$text = $el->getAttributeNode('attr')->firstChild;
+$el->setAttributeNS(null, 'attr', 'new');
+echo "no namespace, detached: ";
+var_dump($text->parentNode === null);
+echo "no namespace, text: ";
+var_dump($text->textContent);
+echo "no namespace, new value: ";
+var_dump($el->getAttribute('attr'));
+
+?>
+--EXPECT--
+prefixed, detached: bool(true)
+prefixed, text: string(3) "old"
+prefixed, new value: string(3) "new"
+no namespace, detached: bool(true)
+no namespace, text: string(3) "old"
+no namespace, new value: string(3) "new"