From 770707111d626ff57d6a66016e2a3322fd2ad9a8 Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Mon, 13 Oct 2025 20:05:16 +0000 Subject: [PATCH 1/4] Add `#[NotSerializable]` attribute to prevent serialization of classes --- .../attributes/not_serializable/001-base.phpt | 29 +++++++++++++++++ .../not_serializable/002-inheritance.phpt | 31 +++++++++++++++++++ Zend/zend_attributes.c | 4 +++ Zend/zend_attributes.stub.php | 6 ++++ Zend/zend_attributes_arginfo.h | 17 +++++++++- Zend/zend_compile.c | 5 +++ 6 files changed, 91 insertions(+), 1 deletion(-) create mode 100644 Zend/tests/attributes/not_serializable/001-base.phpt create mode 100644 Zend/tests/attributes/not_serializable/002-inheritance.phpt diff --git a/Zend/tests/attributes/not_serializable/001-base.phpt b/Zend/tests/attributes/not_serializable/001-base.phpt new file mode 100644 index 0000000000000..659d64a089841 --- /dev/null +++ b/Zend/tests/attributes/not_serializable/001-base.phpt @@ -0,0 +1,29 @@ +--TEST-- +#[NotSerializable] basic behavior +--FILE-- +getMessage(), "\n"; +} + +try { + $data = 'O:3:"Foo":1:{s:1:"x";i:42;}'; + unserialize($data); + echo "Should not reach here\n"; +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECTF-- +Serialization of 'Foo' is not allowed +Unserialization of 'Foo' is not allowed diff --git a/Zend/tests/attributes/not_serializable/002-inheritance.phpt b/Zend/tests/attributes/not_serializable/002-inheritance.phpt new file mode 100644 index 0000000000000..e873b2ea771d2 --- /dev/null +++ b/Zend/tests/attributes/not_serializable/002-inheritance.phpt @@ -0,0 +1,31 @@ +--TEST-- +#[NotSerializable] basic behavior +--FILE-- +getMessage(), "\n"; +} + +try { + $data = 'O:3:"Bar":1:{s:1:"x";i:42;}'; + unserialize($data); + echo "Should not reach here\n"; +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECTF-- +Serialization of 'Bar' is not allowed +Unserialization of 'Bar' is not allowed diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index b69e192701e48..b8730e2f2209d 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -33,6 +33,7 @@ ZEND_API zend_class_entry *zend_ce_override; ZEND_API zend_class_entry *zend_ce_deprecated; ZEND_API zend_class_entry *zend_ce_nodiscard; ZEND_API zend_class_entry *zend_ce_delayed_target_validation; +ZEND_API zend_class_entry *zend_ce_not_serializable; static zend_object_handlers attributes_object_handlers_sensitive_parameter_value; @@ -606,6 +607,9 @@ void zend_register_attribute_ce(void) zend_ce_delayed_target_validation = register_class_DelayedTargetValidation(); attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation); + + zend_ce_not_serializable = register_class_NotSerializable(); + zend_mark_internal_attribute(zend_ce_not_serializable); } void zend_attributes_shutdown(void) diff --git a/Zend/zend_attributes.stub.php b/Zend/zend_attributes.stub.php index ded9c89593a36..d07ca74fcec09 100644 --- a/Zend/zend_attributes.stub.php +++ b/Zend/zend_attributes.stub.php @@ -103,3 +103,9 @@ public function __construct(?string $message = null) {} */ #[Attribute(Attribute::TARGET_ALL)] final class DelayedTargetValidation {} + +/** + * @strict-properties + */ +#[Attribute(Attribute::TARGET_CLASS)] +final class NotSerializable {} diff --git a/Zend/zend_attributes_arginfo.h b/Zend/zend_attributes_arginfo.h index ec8d8de4ee508..72a8bc3824ebe 100644 --- a/Zend/zend_attributes_arginfo.h +++ b/Zend/zend_attributes_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: b868cb33f41d9442f42d0cec84e33fcc09f5d88c */ + * Stub hash: ef3a21a6e698ac7755676ec43f5ba0daac63bb76 */ ZEND_BEGIN_ARG_INFO_EX(arginfo_class_Attribute___construct, 0, 0, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, flags, IS_LONG, 0, "Attribute::TARGET_ALL") @@ -291,3 +291,18 @@ static zend_class_entry *register_class_DelayedTargetValidation(void) return class_entry; } + +static zend_class_entry *register_class_NotSerializable(void) +{ + zend_class_entry ce, *class_entry; + + INIT_CLASS_ENTRY(ce, "NotSerializable", NULL); + class_entry = zend_register_internal_class_with_flags(&ce, NULL, ZEND_ACC_FINAL|ZEND_ACC_NO_DYNAMIC_PROPERTIES); + + zend_string *attribute_name_Attribute_class_NotSerializable_0 = zend_string_init_interned("Attribute", sizeof("Attribute") - 1, true); + zend_attribute *attribute_Attribute_class_NotSerializable_0 = zend_add_class_attribute(class_entry, attribute_name_Attribute_class_NotSerializable_0, 1); + zend_string_release_ex(attribute_name_Attribute_class_NotSerializable_0, true); + ZVAL_LONG(&attribute_Attribute_class_NotSerializable_0->args[0].value, ZEND_ATTRIBUTE_TARGET_CLASS); + + return class_entry; +} diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index d61c2df0a3555..e46c736300185 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9346,6 +9346,11 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) if (decl->child[3]) { zend_compile_attributes(&ce->attributes, decl->child[3], 0, ZEND_ATTRIBUTE_TARGET_CLASS, 0); + + zend_attribute *not_serializable = zend_get_attribute_str(ce->attributes, "notserializable", sizeof("notserializable")-1); + if (not_serializable) { + ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + } } if (implements_ast) { From 7aa8f16cd6171040351f4811d4749a304692913b Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Mon, 13 Oct 2025 22:44:03 +0000 Subject: [PATCH 2/4] Prevent unserialization of enums marked as `ZEND_ACC_NOT_SERIALIZABLE` --- ext/standard/var_unserializer.re | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index 353c7086d4304..414040965aa0c 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -1394,6 +1394,12 @@ object ":" uiv ":" ["] { goto fail; } + if (ce->ce_flags & ZEND_ACC_NOT_SERIALIZABLE) { + zend_throw_exception_ex(NULL, 0, "Unserialization of '%s' is not allowed", + ZSTR_VAL(ce->name)); + goto fail; + } + YYCURSOR += 2; *p = YYCURSOR; From 4eb56043439fe8e4135bd24b040296c39a9ff502 Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Mon, 13 Oct 2025 22:44:29 +0000 Subject: [PATCH 3/4] Validate `#[NotSerializable]` attribute and prevent its use on traits and interfaces --- Zend/zend_attributes.c | 15 ++++++++++++++- Zend/zend_compile.c | 5 ----- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Zend/zend_attributes.c b/Zend/zend_attributes.c index b8730e2f2209d..39d7f6f33714d 100644 --- a/Zend/zend_attributes.c +++ b/Zend/zend_attributes.c @@ -243,6 +243,18 @@ static zend_string *validate_nodiscard( return NULL; } +static zend_string *validate_not_serializable( + zend_attribute *attr, uint32_t target, zend_class_entry *scope) +{ + if (scope->ce_flags & (ZEND_ACC_TRAIT|ZEND_ACC_INTERFACE)) { + const char *type = zend_get_object_type_case(scope, false); + return zend_strpprintf(0, "Cannot apply #[\\NotSerializable] to %s %s", type, ZSTR_VAL(scope->name)); + } + + scope->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; + return NULL; +} + ZEND_METHOD(NoDiscard, __construct) { zend_string *message = NULL; @@ -609,7 +621,8 @@ void zend_register_attribute_ce(void) attr = zend_mark_internal_attribute(zend_ce_delayed_target_validation); zend_ce_not_serializable = register_class_NotSerializable(); - zend_mark_internal_attribute(zend_ce_not_serializable); + attr = zend_mark_internal_attribute(zend_ce_not_serializable); + attr->validator = validate_not_serializable; } void zend_attributes_shutdown(void) diff --git a/Zend/zend_compile.c b/Zend/zend_compile.c index e46c736300185..d61c2df0a3555 100644 --- a/Zend/zend_compile.c +++ b/Zend/zend_compile.c @@ -9346,11 +9346,6 @@ static void zend_compile_class_decl(znode *result, zend_ast *ast, bool toplevel) if (decl->child[3]) { zend_compile_attributes(&ce->attributes, decl->child[3], 0, ZEND_ATTRIBUTE_TARGET_CLASS, 0); - - zend_attribute *not_serializable = zend_get_attribute_str(ce->attributes, "notserializable", sizeof("notserializable")-1); - if (not_serializable) { - ce->ce_flags |= ZEND_ACC_NOT_SERIALIZABLE; - } } if (implements_ast) { From 7543bf1012c72f3a9e7a4382d41b5155429ed12c Mon Sep 17 00:00:00 2001 From: Dmytro Kulyk Date: Mon, 13 Oct 2025 22:44:48 +0000 Subject: [PATCH 4/4] Extend tests for `#[NotSerializable]` attribute to cover traits, interfaces, enums, and inheritance scenarios. --- .../not_serializable/002-inheritance.phpt | 2 +- .../not_serializable/003-interface.phpt | 13 +++++++++ .../not_serializable/004-traits.phpt | 13 +++++++++ .../attributes/not_serializable/005-enum.phpt | 29 +++++++++++++++++++ .../serialize/ref_to_failed_serialize.phpt | 4 +-- 5 files changed, 58 insertions(+), 3 deletions(-) create mode 100644 Zend/tests/attributes/not_serializable/003-interface.phpt create mode 100644 Zend/tests/attributes/not_serializable/004-traits.phpt create mode 100644 Zend/tests/attributes/not_serializable/005-enum.phpt diff --git a/Zend/tests/attributes/not_serializable/002-inheritance.phpt b/Zend/tests/attributes/not_serializable/002-inheritance.phpt index e873b2ea771d2..b8e8f709e1a8e 100644 --- a/Zend/tests/attributes/not_serializable/002-inheritance.phpt +++ b/Zend/tests/attributes/not_serializable/002-inheritance.phpt @@ -1,5 +1,5 @@ --TEST-- -#[NotSerializable] basic behavior +#[NotSerializable] inheritance behavior --FILE-- +--EXPECTF-- +Fatal error: Cannot apply #[\NotSerializable] to interface Foo in %s on line %d diff --git a/Zend/tests/attributes/not_serializable/004-traits.phpt b/Zend/tests/attributes/not_serializable/004-traits.phpt new file mode 100644 index 0000000000000..6ec39b0eda3be --- /dev/null +++ b/Zend/tests/attributes/not_serializable/004-traits.phpt @@ -0,0 +1,13 @@ +--TEST-- +#[NotSerializable] traits behavior +--FILE-- + +--EXPECTF-- +Fatal error: Cannot apply #[\NotSerializable] to trait Foo in %s on line %d diff --git a/Zend/tests/attributes/not_serializable/005-enum.phpt b/Zend/tests/attributes/not_serializable/005-enum.phpt new file mode 100644 index 0000000000000..df5359c01d289 --- /dev/null +++ b/Zend/tests/attributes/not_serializable/005-enum.phpt @@ -0,0 +1,29 @@ +--TEST-- +#[NotSerializable] enum behavior +--FILE-- +getMessage(), "\n"; +} + +try { + $data = 'E:7:"Foo:BAR";'; + unserialize($data); + echo "Should not reach here\n"; +} catch (Throwable $e) { + echo $e->getMessage(), "\n"; +} + +?> +--EXPECTF-- +Serialization of 'Foo' is not allowed +Unserialization of 'Foo' is not allowed diff --git a/ext/standard/tests/serialize/ref_to_failed_serialize.phpt b/ext/standard/tests/serialize/ref_to_failed_serialize.phpt index 294f30b7b6c12..f5ff2c2f51f8e 100644 --- a/ext/standard/tests/serialize/ref_to_failed_serialize.phpt +++ b/ext/standard/tests/serialize/ref_to_failed_serialize.phpt @@ -3,7 +3,7 @@ References to objects for which Serializable::serialize() returned NULL should u --FILE--