Skip to content

Fix GH-21496: UAF in dom_objects_free_storage.#21500

Draft
devnexen wants to merge 3 commits intophp:PHP-8.4from
devnexen:gh21496
Draft

Fix GH-21496: UAF in dom_objects_free_storage.#21500
devnexen wants to merge 3 commits intophp:PHP-8.4from
devnexen:gh21496

Conversation

@devnexen
Copy link
Member

Cloning a non-document DOM node creates a copy within the same xmlDoc. importStylesheet then passes that original document to xsltParseStylesheetDoc, which strips and frees nodes (including comment nodes) during processing, invalidating PHP objects still referencing them.

Validate that the imported node is a document node before proceeding with the clone.

@devnexen devnexen marked this pull request as ready for review March 23, 2026 08:54
@devnexen devnexen requested a review from ndossche as a code owner March 23, 2026 08:54
@devnexen devnexen linked an issue Mar 23, 2026 that may be closed by this pull request
Copy link
Member

@ndossche ndossche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your analysis of the issue is right. However, I believe your solution is too restrictive.

In practice, I expect that people often pass the root of the document to this function. That works but is apparently not tested.

I believe the solution should be this:

  • Grab the equivalent $ownerDocument of the passed node. You can look at the dom $ownerDocument property implementation to see how to do that.
  • Clone that $ownerDocument.
  • It occurs to me that the clone function can be NULL, so you have to add a NULL check too for Z_OBJ_HANDLER_P(docp, clone_obj)

Copy link
Member

@ndossche ndossche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's better already

RETURN_THROWS();
}

php_dom_create_object((xmlNodePtr) nodep->doc, &owner_zv, php_dom_obj_from_obj(Z_OBJ_P(docp)));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe the last argument may be NULL because you don't necessarily have a DOM object here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NULL creates a disconnected php_libxml_ref_obj which frees the xmlDoc on zval_ptr_dtor while the node still references it — same UAF when $doc is unset. Passing the
node's dom_object shares the existing doc ref.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but the current code will fail with simplexml, as xslt is generic over the different xml wrappers:
Please add this as a test:

$sxe = simplexml_load_string('<container/>');
$proc = new XSLTProcessor();
$proc->importStylesheet($sxe);

To solve this, I think you need to do this:

Suggested change
php_dom_create_object((xmlNodePtr) nodep->doc, &owner_zv, php_dom_obj_from_obj(Z_OBJ_P(docp)));
/* See dom_import_simplexml_common */
dom_object *nodeobj = (dom_object *) ((char *) Z_OBJ_P(docp) - Z_OBJ_HT_P(docp)->offset);
php_dom_create_object((xmlNodePtr) nodep->doc, &owner_zv, nodeobj);

docp = &owner_zv;
}

if (Z_OBJ_HANDLER_P(docp, clone_obj) == NULL) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should likely be checked first, because now you can leak owner_zv if this branch hits.

@ndossche
Copy link
Member

You force pushed to the PR but hitting "compare" shows no changes... ?

@devnexen
Copy link
Member Author

silly me ! I did not push the changes ...

devnexen added a commit to devnexen/php-src that referenced this pull request Mar 24, 2026
Cloning a non-document DOM node creates a copy within the same
xmlDoc. importStylesheet then passes that original document to
xsltParseStylesheetDoc, which may strip and free nodes during
processing, invalidating PHP objects still referencing them.

Resolve the ownerDocument for non-document nodes and clone that
instead.

close phpGH-21500
* stylesheet document otherwise the node proxies will be a mess.
* We will clone the object and detach the libxml internals later. */
zend_object *clone = Z_OBJ_HANDLER_P(docp, clone_obj)(Z_OBJ_P(docp));
if (!clone) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This branch can still leak owner_zv. This can be solved by moving zval_ptr_dtor(&owner_zv); directly after the clone

}

if (Z_OBJ_HANDLER_P(docp, clone_obj) == NULL) {
zend_argument_type_error(1, "must be a cloneable document");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error message is inaccurate, it should probably read:

Suggested change
zend_argument_type_error(1, "must be a cloneable document");
zend_argument_type_error(1, "must be a cloneable node");

XML);
$doc->documentElement->appendChild($comment);
$proc = new XSLTProcessor();
var_dump($proc->importStylesheet($comment));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally you should also test this with $doc unset.

Cloning a non-document DOM node creates a copy within the same
xmlDoc. importStylesheet then passes that original document to
xsltParseStylesheetDoc, which may strip and free nodes during
processing, invalidating PHP objects still referencing them.

Resolve the ownerDocument for non-document nodes and clone that
instead.

close phpGH-21500
@devnexen devnexen marked this pull request as draft March 24, 2026 18:23
NULL creates a disconnected php_libxml_ref_obj which frees the
xmlDoc on zval_ptr_dtor while the node still references it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

UAF Dom dom_objects_free_storage

3 participants