Skip to content

Add max_depth option to unserialize() #4742

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

Closed
wants to merge 4 commits into from
Closed
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
1 change: 1 addition & 0 deletions ext/standard/basic_functions.c
Original file line number Diff line number Diff line change
Expand Up @@ -3667,6 +3667,7 @@ PHP_MINIT_FUNCTION(basic) /* {{{ */
register_html_constants(INIT_FUNC_ARGS_PASSTHRU);
register_string_constants(INIT_FUNC_ARGS_PASSTHRU);

BASIC_MINIT_SUBMODULE(var)
BASIC_MINIT_SUBMODULE(file)
BASIC_MINIT_SUBMODULE(pack)
BASIC_MINIT_SUBMODULE(browscap)
Expand Down
1 change: 1 addition & 0 deletions ext/standard/basic_functions.h
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ typedef struct _php_basic_globals {
#endif

int umask;
zend_long unserialize_max_depth;
} php_basic_globals;

#ifdef ZTS
Expand Down
5 changes: 5 additions & 0 deletions ext/standard/php_var.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include "ext/standard/basic_functions.h"
#include "zend_smart_str_public.h"

PHP_MINIT_FUNCTION(var);
PHP_FUNCTION(var_dump);
PHP_FUNCTION(var_export);
PHP_FUNCTION(debug_zval_dump);
Expand Down Expand Up @@ -50,6 +51,10 @@ 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 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);
PHPAPI zend_long php_var_unserialize_get_cur_depth(php_unserialize_data_t d);

#define PHP_VAR_SERIALIZE_INIT(d) \
(d) = php_var_serialize_init()
Expand Down
159 changes: 159 additions & 0 deletions ext/standard/tests/serialize/max_depth.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
--TEST--
Bug #78549: Stack overflow due to nested serialized input
--FILE--
<?php

function create_nested_data($depth, $prefix, $suffix, $inner = 'i:0;') {
return str_repeat($prefix, $depth) . $inner . str_repeat($suffix, $depth);
}

echo "Invalid max_depth:\n";
var_dump(unserialize('i:0;', ['max_depth' => 'foo']));
var_dump(unserialize('i:0;', ['max_depth' => -1]));

echo "Array:\n";
var_dump(unserialize(
create_nested_data(128, 'a:1:{i:0;', '}'),
['max_depth' => 128]
) !== false);
var_dump(unserialize(
create_nested_data(129, 'a:1:{i:0;', '}'),
['max_depth' => 128]
));

echo "Object:\n";
var_dump(unserialize(
create_nested_data(128, 'O:8:"stdClass":1:{i:0;', '}'),
['max_depth' => 128]
) !== false);
var_dump(unserialize(
create_nested_data(129, 'O:8:"stdClass":1:{i:0;', '}'),
['max_depth' => 128]
));

// Default depth is 4096
echo "Default depth:\n";
var_dump(unserialize(create_nested_data(4096, 'a:1:{i:0;', '}')) !== false);
var_dump(unserialize(create_nested_data(4097, 'a:1:{i:0;', '}')));

// Depth can also be adjusted using ini setting
echo "Ini setting:\n";
ini_set("unserialize_max_depth", 128);
var_dump(unserialize(create_nested_data(128, 'a:1:{i:0;', '}')) !== false);
var_dump(unserialize(create_nested_data(129, 'a:1:{i:0;', '}')));

// But an explicitly specified depth still takes precedence
echo "Ini setting overridden:\n";
var_dump(unserialize(
create_nested_data(256, 'a:1:{i:0;', '}'),
['max_depth' => 256]
) !== false);
var_dump(unserialize(
create_nested_data(257, 'a:1:{i:0;', '}'),
['max_depth' => 256]
));

// Reset ini setting to a large value,
// so it's clear that it won't be used in the following.
ini_set("unserialize_max_depth", 4096);

class Test implements Serializable {
public function serialize() {
return '';
}
public function unserialize($str) {
// Should fail, due to combined nesting level
var_dump(unserialize(create_nested_data(129, 'a:1:{i:0;', '}')));
// Should succeeed, below combined nesting level
var_dump(unserialize(create_nested_data(128, 'a:1:{i:0;', '}')) !== false);
}
}
echo "Nested unserialize combined depth limit:\n";
var_dump(is_array(unserialize(
create_nested_data(128, 'a:1:{i:0;', '}', 'C:4:"Test":0:{}'),
['max_depth' => 256]
)));

class Test2 implements Serializable {
public function serialize() {
return '';
}
public function unserialize($str) {
// If depth limit is overridden, the depth should be counted
// from zero again.
var_dump(unserialize(
create_nested_data(257, 'a:1:{i:0;', '}'),
['max_depth' => 256]
));
var_dump(unserialize(
create_nested_data(256, 'a:1:{i:0;', '}'),
['max_depth' => 256]
) !== false);
}
}
echo "Nested unserialize overridden depth limit:\n";
var_dump(is_array(unserialize(
create_nested_data(64, 'a:1:{i:0;', '}', 'C:5:"Test2":0:{}'),
['max_depth' => 128]
)));

?>
--EXPECTF--
Invalid max_depth:

Warning: unserialize(): max_depth should be int in %s on line %d
bool(false)

Warning: unserialize(): max_depth cannot be negative in %s on line %d
bool(false)
Array:
bool(true)

Warning: unserialize(): Maximum depth of 128 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 1157 of 1294 bytes in %s on line %d
bool(false)
Object:
bool(true)

Warning: unserialize(): Maximum depth of 128 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 2834 of 2971 bytes in %s on line %d
bool(false)
Default depth:
bool(true)

Warning: unserialize(): Maximum depth of 4096 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 36869 of 40974 bytes in %s on line %d
bool(false)
Ini setting:
bool(true)

Warning: unserialize(): Maximum depth of 128 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 1157 of 1294 bytes in %s on line %d
bool(false)
Ini setting overridden:
bool(true)

Warning: unserialize(): Maximum depth of 256 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 2309 of 2574 bytes in %s on line %d
bool(false)
Nested unserialize combined depth limit:

Warning: unserialize(): Maximum depth of 256 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 1157 of 1294 bytes in %s on line %d
bool(false)
bool(true)
bool(true)
Nested unserialize overridden depth limit:

Warning: unserialize(): Maximum depth of 256 exceeded. The depth limit can be changed using the max_depth unserialize() option or the unserialize_max_depth ini setting in %s on line %d

Notice: unserialize(): Error at offset 2309 of 2574 bytes in %s on line %d
bool(false)
bool(true)
bool(true)
53 changes: 44 additions & 9 deletions ext/standard/var.c
Original file line number Diff line number Diff line change
Expand Up @@ -1174,17 +1174,18 @@ PHP_FUNCTION(serialize)
}
/* }}} */

/* {{{ proto mixed unserialize(string variable_representation[, array allowed_classes])
/* {{{ proto mixed unserialize(string variable_representation[, array options])
Takes a string representation of variable and recreates it */
PHP_FUNCTION(unserialize)
{
char *buf = NULL;
size_t buf_len;
const unsigned char *p;
php_unserialize_data_t var_hash;
zval *options = NULL, *classes = NULL;
zval *options = NULL;
zval *retval;
HashTable *class_hash = NULL, *prev_class_hash;
zend_long prev_max_depth, prev_cur_depth;

ZEND_PARSE_PARAMETERS_START(1, 2)
Z_PARAM_STRING(buf, buf_len)
Expand All @@ -1200,12 +1201,16 @@ PHP_FUNCTION(unserialize)
PHP_VAR_UNSERIALIZE_INIT(var_hash);

prev_class_hash = php_var_unserialize_get_allowed_classes(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) {
classes = zend_hash_str_find(Z_ARRVAL_P(options), "allowed_classes", sizeof("allowed_classes")-1);
zval *classes, *max_depth;

classes = zend_hash_str_find_deref(Z_ARRVAL_P(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) {
php_error_docref(NULL, E_WARNING, "allowed_classes option should be array or boolean");
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
RETURN_FALSE;
RETVAL_FALSE;
goto cleanup;
}

if(classes && (Z_TYPE_P(classes) == IS_ARRAY || !zend_is_true(classes))) {
Expand All @@ -1225,12 +1230,29 @@ PHP_FUNCTION(unserialize)

/* Exception during string conversion. */
if (EG(exception)) {
zend_hash_destroy(class_hash);
FREE_HASHTABLE(class_hash);
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);
goto cleanup;
}
}
php_var_unserialize_set_allowed_classes(var_hash, class_hash);

max_depth = zend_hash_str_find_deref(Z_ARRVAL_P(options), "max_depth", sizeof("max_depth") - 1);
if (max_depth) {
if (Z_TYPE_P(max_depth) != IS_LONG) {
php_error_docref(NULL, E_WARNING, "max_depth should be int");
RETVAL_FALSE;
goto cleanup;
}
if (Z_LVAL_P(max_depth) < 0) {
php_error_docref(NULL, E_WARNING, "max_depth cannot be negative");
RETVAL_FALSE;
goto cleanup;
}

php_var_unserialize_set_max_depth(var_hash, Z_LVAL_P(max_depth));
/* If the max_depth for a nested unserialize() call has been overridden,
* start counting from zero again (for the nested call only). */
php_var_unserialize_set_cur_depth(var_hash, 0);
}
}

if (BG(unserialize).level > 1) {
Expand All @@ -1254,13 +1276,16 @@ PHP_FUNCTION(unserialize)
gc_check_possible_root(ref);
}

cleanup:
if (class_hash) {
zend_hash_destroy(class_hash);
FREE_HASHTABLE(class_hash);
}

/* Reset to previous allowed_classes in case this is a nested call */
/* 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_max_depth(var_hash, prev_max_depth);
php_var_unserialize_set_cur_depth(var_hash, prev_cur_depth);
PHP_VAR_UNSERIALIZE_DESTROY(var_hash);

/* Per calling convention we must not return a reference here, so unwrap. We're doing this at
Expand Down Expand Up @@ -1299,3 +1324,13 @@ PHP_FUNCTION(memory_get_peak_usage) {
RETURN_LONG(zend_memory_peak_usage(real_usage));
}
/* }}} */

PHP_INI_BEGIN()
STD_PHP_INI_ENTRY("unserialize_max_depth", "4096", PHP_INI_ALL, OnUpdateLong, unserialize_max_depth, php_basic_globals, basic_globals)
PHP_INI_END()

PHP_MINIT_FUNCTION(var)
{
REGISTER_INI_ENTRIES();
return SUCCESS;
}
Loading