Skip to content

Add allowed_classes_callback to unserialize() #19087

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ext/standard/php_var.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
162 changes: 162 additions & 0 deletions ext/standard/tests/serialize/__serialize_008.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
--TEST--
unserialize allowed_classes_callback
--FILE--
<?php

class TestClass {
}

class AnotherTestClass {
}

// Basic usage
$serialized = serialize(
[
new TestClass(),
new TestClass(),
]
);

$dummy = unserialize(
$serialized,
[
'allowed_classes_callback' => 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"
31 changes: 31 additions & 0 deletions ext/standard/tests/serialize/__serialize_009.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
unserialize allowed_classes_callback blocking unserialize
--FILE--
<?php

class TestClass {
}

$serialized = serialize(
[
new TestClass(),
new TestClass(),
]
);

$dummy = unserialize(
$serialized,
[
'allowed_classes_callback' => 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
13 changes: 11 additions & 2 deletions ext/standard/var.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
48 changes: 41 additions & 7 deletions ext/standard/var_unserializer.re
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -1187,15 +1221,15 @@ 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;
}
}

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);
Expand Down