diff --git a/ext/standard/php_var.h b/ext/standard/php_var.h index d70bbfed814a0..b1216606816b3 100644 --- a/ext/standard/php_var.h +++ b/ext/standard/php_var.h @@ -42,6 +42,8 @@ PHPAPI php_unserialize_data_t php_var_unserialize_init(void); PHPAPI void php_var_unserialize_destroy(php_unserialize_data_t d); PHPAPI HashTable *php_var_unserialize_get_allowed_classes(php_unserialize_data_t d); PHPAPI void php_var_unserialize_set_allowed_classes(php_unserialize_data_t d, HashTable *classes); +PHPAPI zval *php_var_unserialize_get_allowed_classes_callback(php_unserialize_data_t d); +PHPAPI void php_var_unserialize_set_allowed_classes_callback(php_unserialize_data_t d, zval *callback); PHPAPI void php_var_unserialize_set_max_depth(php_unserialize_data_t d, zend_long max_depth); PHPAPI zend_long php_var_unserialize_get_max_depth(php_unserialize_data_t d); PHPAPI void php_var_unserialize_set_cur_depth(php_unserialize_data_t d, zend_long cur_depth); diff --git a/ext/standard/tests/serialize/__serialize_008.phpt b/ext/standard/tests/serialize/__serialize_008.phpt new file mode 100644 index 0000000000000..210682257a32f --- /dev/null +++ b/ext/standard/tests/serialize/__serialize_008.phpt @@ -0,0 +1,162 @@ +--TEST-- +unserialize allowed_classes_callback +--FILE-- + function ($className) { + var_dump($className); + return true; + } + ] +); + +var_dump($dummy); + + +// allowed_classes takes precedent to allowed_classes_callback, which is never called in this case +$dummy = unserialize( + $serialized, + [ + 'allowed_classes' => ['TestClass'], + 'allowed_classes_callback' => function ($className) { + var_dump($className); + return true; + } + ] +); + +var_dump($dummy); + + +// unserialize() blocked class +$dummy = unserialize( + $serialized, + [ + 'allowed_classes_callback' => function ($className) { + return false; + } + ] +); + +var_dump($dummy); + +// Nested unserialize() one is allowed, the second blocked +$flip = false; +$dummy = unserialize( + $serialized, + [ + 'allowed_classes_callback' => function ($className) use (&$flip) { + $serialized = serialize( + [ + new AnotherTestClass(), + ] + ); + + $dummy = unserialize( + $serialized, + [ + 'allowed_classes_callback' => function ($className) use (&$flip) { + echo 'Nested: '; + var_dump($className); + $flip = !$flip; + return $flip; + } + ] + ); + + echo 'Nested: '; + var_dump($dummy); + return true; + } + ] +); + +var_dump($dummy); + + +// throw from inside the callback +try { + $dummy = unserialize( + $serialized, + [ + 'allowed_classes_callback' => function ($className) { + throw new RuntimeException('Better not unserialize this'); + } + ] + ); +} catch (RuntimeException $e) { + var_dump($e->getMessage()); +} + +?> +--EXPECT-- +string(9) "TestClass" +string(9) "TestClass" +array(2) { + [0]=> + object(TestClass)#1 (0) { + } + [1]=> + object(TestClass)#3 (0) { + } +} +array(2) { + [0]=> + object(TestClass)#4 (0) { + } + [1]=> + object(TestClass)#5 (0) { + } +} +array(2) { + [0]=> + object(__PHP_Incomplete_Class)#1 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(9) "TestClass" + } + [1]=> + object(__PHP_Incomplete_Class)#2 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(9) "TestClass" + } +} +Nested: string(16) "AnotherTestClass" +Nested: array(1) { + [0]=> + object(AnotherTestClass)#3 (0) { + } +} +Nested: string(16) "AnotherTestClass" +Nested: array(1) { + [0]=> + object(__PHP_Incomplete_Class)#6 (1) { + ["__PHP_Incomplete_Class_Name"]=> + string(16) "AnotherTestClass" + } +} +array(2) { + [0]=> + object(TestClass)#3 (0) { + } + [1]=> + object(TestClass)#6 (0) { + } +} +string(27) "Better not unserialize this" \ No newline at end of file diff --git a/ext/standard/tests/serialize/__serialize_009.phpt b/ext/standard/tests/serialize/__serialize_009.phpt new file mode 100644 index 0000000000000..d9a1844a78bec --- /dev/null +++ b/ext/standard/tests/serialize/__serialize_009.phpt @@ -0,0 +1,31 @@ +--TEST-- +unserialize allowed_classes_callback blocking unserialize +--FILE-- + function ($className) { + return 0; + } + ] +); + +?> +--EXPECTF-- +Fatal error: Uncaught TypeError: "allowed_classes_callback" must return bool, int given in %s__serialize_009.php:%d +Stack trace: +#0 %s__serialize_009.php(%s): unserialize('a:2:{i:0;O:9:"T...', Array) +#1 {main} + thrown in %s__serialize_009.php on line %d diff --git a/ext/standard/var.c b/ext/standard/var.c index 1c2b0eb164a1c..3b0209251b10b 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -1381,7 +1381,7 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co { const unsigned char *p; php_unserialize_data_t var_hash; - zval *retval; + zval *retval, *prev_class_callback; HashTable *class_hash = NULL, *prev_class_hash; zend_long prev_max_depth, prev_cur_depth; @@ -1393,10 +1393,11 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co PHP_VAR_UNSERIALIZE_INIT(var_hash); prev_class_hash = php_var_unserialize_get_allowed_classes(var_hash); + prev_class_callback = php_var_unserialize_get_allowed_classes_callback(var_hash); prev_max_depth = php_var_unserialize_get_max_depth(var_hash); prev_cur_depth = php_var_unserialize_get_cur_depth(var_hash); if (options != NULL) { - zval *classes, *max_depth; + zval *classes, *classes_callback, *max_depth; classes = zend_hash_str_find_deref(options, "allowed_classes", sizeof("allowed_classes")-1); if (classes && Z_TYPE_P(classes) != IS_ARRAY && Z_TYPE_P(classes) != IS_TRUE && Z_TYPE_P(classes) != IS_FALSE) { @@ -1435,6 +1436,13 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co } php_var_unserialize_set_allowed_classes(var_hash, class_hash); + classes_callback = zend_hash_str_find_deref(options, "allowed_classes_callback", sizeof("allowed_classes_callback")-1); + if (classes_callback && !zend_is_callable(classes_callback, IS_CALLABLE_SUPPRESS_DEPRECATIONS, NULL)) { + zend_type_error("%s(): Option \"allowed_classes_callback\" must be a valid callback", function_name); + goto cleanup; + } + php_var_unserialize_set_allowed_classes_callback(var_hash, classes_callback); + max_depth = zend_hash_str_find_deref(options, "max_depth", sizeof("max_depth") - 1); if (max_depth) { if (Z_TYPE_P(max_depth) != IS_LONG) { @@ -1490,6 +1498,7 @@ PHPAPI void php_unserialize_with_options(zval *return_value, const char *buf, co /* Reset to previous options in case this is a nested call */ php_var_unserialize_set_allowed_classes(var_hash, prev_class_hash); + php_var_unserialize_set_allowed_classes_callback(var_hash, prev_class_callback); php_var_unserialize_set_max_depth(var_hash, prev_max_depth); php_var_unserialize_set_cur_depth(var_hash, prev_cur_depth); PHP_VAR_UNSERIALIZE_DESTROY(var_hash); diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index cbd457e16fdb1..49125e6f3eb40 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -51,6 +51,7 @@ struct php_unserialize_data { var_dtor_entries *first_dtor; var_dtor_entries *last_dtor; HashTable *allowed_classes; + zval *allowed_classes_callback; HashTable *ref_props; zend_long cur_depth; zend_long max_depth; @@ -65,6 +66,7 @@ PHPAPI php_unserialize_data_t php_var_unserialize_init(void) { d->last = &d->entries; d->first_dtor = d->last_dtor = NULL; d->allowed_classes = NULL; + d->allowed_classes_callback = NULL; d->ref_props = NULL; d->cur_depth = 0; d->max_depth = BG(unserialize_max_depth); @@ -99,6 +101,13 @@ PHPAPI void php_var_unserialize_set_allowed_classes(php_unserialize_data_t d, Ha d->allowed_classes = classes; } +PHPAPI zval *php_var_unserialize_get_allowed_classes_callback(php_unserialize_data_t d) { + return d->allowed_classes_callback; +} +PHPAPI void php_var_unserialize_set_allowed_classes_callback(php_unserialize_data_t d, zval *callback) { + d->allowed_classes_callback = callback; +} + PHPAPI void php_var_unserialize_set_max_depth(php_unserialize_data_t d, zend_long max_depth) { d->max_depth = max_depth; } @@ -359,18 +368,43 @@ static zend_string *unserialize_str(const unsigned char **p, size_t len, size_t } static inline int unserialize_allowed_class( - zend_string *lcname, php_unserialize_data_t *var_hashx) + zend_string *lcname, zend_string *class_name, php_unserialize_data_t *var_hashx) { HashTable *classes = (*var_hashx)->allowed_classes; + zval args[1]; + zval retval; - if(classes == NULL) { + if(classes == NULL && (*var_hashx)->allowed_classes_callback == NULL) { return 1; } - if(!zend_hash_num_elements(classes)) { - return 0; + + if (classes != NULL && zend_hash_num_elements(classes) && zend_hash_exists(classes, lcname)) { + return 1; + } + + /* Check for allowed classes callback */ + if ((*var_hashx)->allowed_classes_callback) { + ZVAL_STR(&args[0], class_name); + BG(serialize_lock)++; + call_user_function(NULL, NULL, (*var_hashx)->allowed_classes_callback, &retval, 1, args); + BG(serialize_lock)--; + + if (EG(exception)) { + return 0; + } + + if (Z_TYPE(retval) == IS_TRUE) { + zval_ptr_dtor(&retval); + return 1; + } + + if (Z_TYPE(retval) != IS_FALSE) { + zend_type_error("\"allowed_classes_callback\" must return bool, %s given", zend_zval_value_name(&retval)); + } + zval_ptr_dtor(&retval); } - return zend_hash_exists(classes, lcname); + return 0; } #define YYFILL(n) do { } while (0) @@ -1187,7 +1221,7 @@ object ":" uiv ":" ["] { do { zend_string *lc_name; - if (!(*var_hash)->allowed_classes && ZSTR_HAS_CE_CACHE(class_name)) { + if (!(*var_hash)->allowed_classes && !(*var_hash)->allowed_classes_callback && ZSTR_HAS_CE_CACHE(class_name)) { ce = ZSTR_GET_CE_CACHE(class_name); if (ce) { break; @@ -1195,7 +1229,7 @@ object ":" uiv ":" ["] { } lc_name = zend_string_tolower(class_name); - if(!unserialize_allowed_class(lc_name, var_hash)) { + if(!unserialize_allowed_class(lc_name, class_name, var_hash)) { zend_string_release_ex(lc_name, 0); if (!zend_is_valid_class_name(class_name)) { zend_string_release_ex(class_name, 0);