From aede6ade372bfa4bc008111b4549d8e5e17e5134 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 29 Sep 2026 21:45:47 +0200 Subject: [PATCH] Reject property names that start with a NUL byte deepclone_from_array() created properties whose name starts with a NUL byte, which aren't legal PHP: the engine refuses to access them. They're now rejected, eagerly for lazy objects too. An array cast can give such names to a stdClass, which deepclone_to_array() keeps exporting as they are, like serialize(): checking them would cost every stdClass, and more in the polyfill. --- CHANGELOG.md | 4 ++++ deepclone.c | 8 ++++++- tests/deepclone_nul_property_names.phpt | 31 +++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 tests/deepclone_nul_property_names.phpt diff --git a/CHANGELOG.md b/CHANGELOG.md index 75bc0a5..e3a26d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 returning `null`. - On PHP 8.4+, `deepclone_from_array()` checks the shape of the named closures that lazy objects hold right away, instead of when these are first used. +- `deepclone_from_array()` rejects property names that start with a NUL byte, + which aren't legal PHP, instead of creating them. An array cast can give + such names to a `stdClass`, which `deepclone_to_array()` exports as they + are, like `serialize()`. ## [0.8.6] - 2026-09-29 diff --git a/deepclone.c b/deepclone.c index 0ae02da..d5ef5c5 100644 --- a/deepclone.c +++ b/deepclone.c @@ -4577,7 +4577,7 @@ static bool dc_lazy_index_build(dc_lazy_ctx *ctx, HashTable *properties_ht, Hash zend_ulong prop_idx; zval *id_values; ZEND_HASH_FOREACH_KEY_VAL(Z_ARRVAL_P(scope_props), prop_idx, prop_name, id_values) { - if (Z_TYPE_P(id_values) != IS_ARRAY) { + if (Z_TYPE_P(id_values) != IS_ARRAY || (prop_name && ZSTR_LEN(prop_name) && ZSTR_VAL(prop_name)[0] == '\0')) { continue; /* phase 8 reports it */ } bool prop_is_numeric = (prop_name == NULL); @@ -5658,6 +5658,12 @@ PHP_FUNCTION(deepclone_from_array) EG(fake_scope) = old_scope; DC_INVALID("deepclone_from_array(): Argument #1 ($data) \"properties\" value for \"%s::%s\" must be of type array, %s given", ZSTR_VAL(scope_name), ZSTR_VAL(prop_name), zend_zval_value_name(id_values)); } + if (UNEXPECTED(ZSTR_LEN(prop_name) && ZSTR_VAL(prop_name)[0] == '\0')) { + /* Not a legal name, which the engine refuses to access, + * even if an array cast can give it to a stdClass */ + EG(fake_scope) = old_scope; + DC_INVALID("deepclone_from_array(): Argument #1 ($data) \"properties\" names of scope \"%s\" cannot start with \"\\0\"", ZSTR_VAL(scope_name)); + } /* Get resolve markers for this property */ HashTable *resolve_ids = NULL; diff --git a/tests/deepclone_nul_property_names.phpt b/tests/deepclone_nul_property_names.phpt new file mode 100644 index 0000000..c72d22d --- /dev/null +++ b/tests/deepclone_nul_property_names.phpt @@ -0,0 +1,31 @@ +--TEST-- +deepclone_from_array() rejects property names that start with a NUL byte +--EXTENSIONS-- +deepclone +--FILE-- + 'stdClass', 'objectMeta' => 1, 'prepared' => 0, 'properties' => ['stdClass' => ["\0x" => [1]]]], + ['classes' => 'A', 'objectMeta' => 1, 'prepared' => 0, 'properties' => ['stdClass' => ["\0A\0a" => [1]]]], + // An array cast can give such names to a stdClass + deepclone_to_array((object) ["\0*\0p" => 1]), +]; + +foreach ($payloads as $payload) { + try { + deepclone_from_array($payload); + } catch (ValueError $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +deepclone_from_array(): Argument #1 ($data) "properties" names of scope "stdClass" cannot start with "\0" +deepclone_from_array(): Argument #1 ($data) "properties" names of scope "stdClass" cannot start with "\0" +deepclone_from_array(): Argument #1 ($data) "properties" names of scope "stdClass" cannot start with "\0"