Commit bda5a4943ec for php.net
commit bda5a4943ec785f9cb2453da32efa0ece34afa53
Author: Ilia Alshanetsky <ilia@ilia.ws>
Date: Sun Aug 9 19:44:01 2026 -0400
Reject NUL bytes in the SimpleXMLElement constructor (#23069)
With dataIsURL set the first argument goes to xmlReadFile as a C string,
so new SimpleXMLElement("/tmp/ok.xml\0evil", 0, true) truncates at the NUL
and quietly loads /tmp/ok.xml. simplexml_load_file() and
SimpleXMLElement::asXML() already declare their path argument as a path;
the constructor took a plain string, so the check never ran.
Closes GH-23069
diff --git a/NEWS b/NEWS
index d7b60866a32..39d5a948e26 100644
--- a/NEWS
+++ b/NEWS
@@ -70,6 +70,8 @@ PHP NEWS
- SimpleXML:
. Fixed integer element offsets that cannot resolve aliasing an existing
element. (iliaal)
+ . SimpleXMLElement::__construct() now raises a ValueError when the $data
+ argument contains NUL bytes. (iliaal)
- Standard:
. Added the "filter.max_filter_count" stream context option for php://filter
diff --git a/UPGRADING b/UPGRADING
index ba4bf182911..e4669740b35 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -200,6 +200,14 @@ PHP 8.6 UPGRADE NOTES
SplFileObject::seek() past EOF now produces the same key() value as
SplTempFileObject; the two previously returned different values.
+- SimpleXML:
+ . SimpleXMLElement::__construct() now raises a ValueError when the $data
+ argument contains NUL bytes, matching simplexml_load_file(). With
+ $dataIsURL set it previously truncated the path at the first NUL byte.
+ Without it the string went to libxml, which at default options rejects a
+ NUL on current versions but accepts the truncated document on older ones
+ and under LIBXML_RECOVER.
+
- Standard:
. array_intersect() with at least two arrays now converts values to strings
while scanning its inputs instead of during sort comparisons. This can
diff --git a/ext/simplexml/simplexml.c b/ext/simplexml/simplexml.c
index 1058c463cc6..828262f18aa 100644
--- a/ext/simplexml/simplexml.c
+++ b/ext/simplexml/simplexml.c
@@ -2283,18 +2283,17 @@ PHP_FUNCTION(simplexml_load_string)
PHP_METHOD(SimpleXMLElement, __construct)
{
php_sxe_object *sxe = Z_SXEOBJ_P(ZEND_THIS);
- char *data;
+ zend_string *data;
zend_string *ns = zend_empty_string;
- size_t data_len;
xmlDocPtr docp;
zend_long options = 0;
bool is_url = false, isprefix = false;
- if (zend_parse_parameters(ZEND_NUM_ARGS(), "s|lbSb", &data, &data_len, &options, &is_url, &ns, &isprefix) == FAILURE) {
+ if (zend_parse_parameters(ZEND_NUM_ARGS(), "P|lbSb", &data, &options, &is_url, &ns, &isprefix) == FAILURE) {
RETURN_THROWS();
}
- if (ZEND_SIZE_T_INT_OVFL(data_len)) {
+ if (ZEND_SIZE_T_INT_OVFL(ZSTR_LEN(data))) {
zend_argument_error(zend_ce_exception, 1, "is too long");
RETURN_THROWS();
}
@@ -2308,7 +2307,7 @@ PHP_METHOD(SimpleXMLElement, __construct)
}
PHP_LIBXML_SANITIZE_GLOBALS(read_file_or_memory);
- docp = is_url ? xmlReadFile(data, NULL, (int)options) : xmlReadMemory(data, (int)data_len, NULL, NULL, (int)options);
+ docp = is_url ? xmlReadFile(ZSTR_VAL(data), NULL, (int)options) : xmlReadMemory(ZSTR_VAL(data), (int)ZSTR_LEN(data), NULL, NULL, (int)options);
PHP_LIBXML_RESTORE_GLOBALS(read_file_or_memory);
if (!docp) {
diff --git a/ext/simplexml/tests/sxe_ctor_nul_path.phpt b/ext/simplexml/tests/sxe_ctor_nul_path.phpt
new file mode 100644
index 00000000000..ac451747a4b
--- /dev/null
+++ b/ext/simplexml/tests/sxe_ctor_nul_path.phpt
@@ -0,0 +1,37 @@
+--TEST--
+SimpleXMLElement constructor rejects NUL bytes in $data
+--EXTENSIONS--
+simplexml
+--FILE--
+<?php
+$tmp = tempnam(sys_get_temp_dir(), 'sxe');
+file_put_contents($tmp, '<r/>');
+$path = $tmp . "\0evil";
+
+try {
+ new SimpleXMLElement($path, 0, true);
+ echo "url mode: loaded\n";
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+ new SimpleXMLElement("<r/>\0evil");
+ echo "data mode: loaded\n";
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+try {
+ simplexml_load_file($path);
+ echo "load_file: loaded\n";
+} catch (Throwable $e) {
+ echo $e::class, ": ", $e->getMessage(), "\n";
+}
+
+unlink($tmp);
+?>
+--EXPECT--
+ValueError: SimpleXMLElement::__construct(): Argument #1 ($data) must not contain any null bytes
+ValueError: SimpleXMLElement::__construct(): Argument #1 ($data) must not contain any null bytes
+ValueError: simplexml_load_file(): Argument #1 ($filename) must not contain any null bytes