diff options
-rwxr-xr-x | ext/standard/basic_functions.c | 1 | ||||
-rw-r--r-- | ext/standard/basic_functions.h | 1 | ||||
-rw-r--r-- | ext/standard/php_var.h | 5 | ||||
-rw-r--r-- | ext/standard/tests/serialize/max_depth.phpt | 159 | ||||
-rw-r--r-- | ext/standard/var.c | 53 | ||||
-rw-r--r-- | ext/standard/var_unserializer.re | 53 | ||||
-rw-r--r-- | php.ini-development | 7 | ||||
-rw-r--r-- | php.ini-production | 7 |
8 files changed, 270 insertions, 16 deletions
diff --git a/ext/standard/basic_functions.c b/ext/standard/basic_functions.c index 5916a1cde3..ff12fceb73 100755 --- a/ext/standard/basic_functions.c +++ b/ext/standard/basic_functions.c @@ -2985,6 +2985,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) diff --git a/ext/standard/basic_functions.h b/ext/standard/basic_functions.h index fd7ccfdfe4..140a51dde2 100644 --- a/ext/standard/basic_functions.h +++ b/ext/standard/basic_functions.h @@ -227,6 +227,7 @@ typedef struct _php_basic_globals { #endif int umask; + zend_long unserialize_max_depth; } php_basic_globals; #ifdef ZTS diff --git a/ext/standard/php_var.h b/ext/standard/php_var.h index e27b9f1110..d9abae86d2 100644 --- a/ext/standard/php_var.h +++ b/ext/standard/php_var.h @@ -20,6 +20,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); @@ -48,6 +49,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() diff --git a/ext/standard/tests/serialize/max_depth.phpt b/ext/standard/tests/serialize/max_depth.phpt new file mode 100644 index 0000000000..4f605d284e --- /dev/null +++ b/ext/standard/tests/serialize/max_depth.phpt @@ -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) diff --git a/ext/standard/var.c b/ext/standard/var.c index 562a1b623c..d862417dbb 100644 --- a/ext/standard/var.c +++ b/ext/standard/var.c @@ -1167,7 +1167,7 @@ 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) { @@ -1175,9 +1175,10 @@ PHP_FUNCTION(unserialize) 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) @@ -1193,12 +1194,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))) { @@ -1218,12 +1223,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) { @@ -1247,13 +1269,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 @@ -1292,3 +1317,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; +} diff --git a/ext/standard/var_unserializer.re b/ext/standard/var_unserializer.re index 8499078a6d..172f414797 100644 --- a/ext/standard/var_unserializer.re +++ b/ext/standard/var_unserializer.re @@ -47,6 +47,8 @@ struct php_unserialize_data { var_dtor_entries *last_dtor; HashTable *allowed_classes; HashTable *ref_props; + zend_long cur_depth; + zend_long max_depth; var_entries entries; }; @@ -59,6 +61,8 @@ PHPAPI php_unserialize_data_t php_var_unserialize_init() { d->first_dtor = d->last_dtor = NULL; d->allowed_classes = NULL; d->ref_props = NULL; + d->cur_depth = 0; + d->max_depth = BG(unserialize_max_depth); d->entries.used_slots = 0; d->entries.next = NULL; if (!BG(serialize_lock)) { @@ -90,6 +94,20 @@ PHPAPI void php_var_unserialize_set_allowed_classes(php_unserialize_data_t d, Ha d->allowed_classes = classes; } +PHPAPI void php_var_unserialize_set_max_depth(php_unserialize_data_t d, zend_long max_depth) { + d->max_depth = max_depth; +} +PHPAPI zend_long php_var_unserialize_get_max_depth(php_unserialize_data_t d) { + return d->max_depth; +} + +PHPAPI void php_var_unserialize_set_cur_depth(php_unserialize_data_t d, zend_long cur_depth) { + d->cur_depth = cur_depth; +} +PHPAPI zend_long php_var_unserialize_get_cur_depth(php_unserialize_data_t d) { + return d->cur_depth; +} + static inline void var_push(php_unserialize_data_t *var_hashx, zval *rval) { var_entries *var_hash = (*var_hashx)->last; @@ -436,6 +454,18 @@ static int php_var_unserialize_internal(UNSERIALIZE_PARAMETER, int as_key); static zend_always_inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTable *ht, zend_long elements, zend_object *obj) { + if (var_hash) { + if ((*var_hash)->max_depth > 0 && (*var_hash)->cur_depth >= (*var_hash)->max_depth) { + php_error_docref(NULL, E_WARNING, + "Maximum depth of " ZEND_LONG_FMT " exceeded. " + "The depth limit can be changed using the max_depth unserialize() option " + "or the unserialize_max_depth ini setting", + (*var_hash)->max_depth); + return 0; + } + (*var_hash)->cur_depth++; + } + while (elements-- > 0) { zval key, *data, d, *old_data; zend_ulong idx; @@ -445,7 +475,7 @@ static zend_always_inline int process_nested_data(UNSERIALIZE_PARAMETER, HashTab if (!php_var_unserialize_internal(&key, p, max, NULL, 1)) { zval_ptr_dtor(&key); - return 0; + goto failure; } data = NULL; @@ -475,7 +505,7 @@ numeric_key: } } else { zval_ptr_dtor(&key); - return 0; + goto failure; } } else { if (EXPECTED(Z_TYPE(key) == IS_STRING)) { @@ -490,7 +520,7 @@ string_key: if (UNEXPECTED(zend_unmangle_property_name_ex(Z_STR(key), &unmangled_class, &unmangled_prop, &unmangled_prop_len) == FAILURE)) { zval_ptr_dtor(&key); - return 0; + goto failure; } unmangled = zend_string_init(unmangled_prop, unmangled_prop_len, 0); @@ -557,13 +587,13 @@ string_key: goto string_key; } else { zval_ptr_dtor(&key); - return 0; + goto failure; } } if (!php_var_unserialize_internal(data, p, max, var_hash, 0)) { zval_ptr_dtor(&key); - return 0; + goto failure; } if (UNEXPECTED(info)) { @@ -571,7 +601,7 @@ string_key: zval_ptr_dtor(data); ZVAL_UNDEF(data); zval_dtor(&key); - return 0; + goto failure; } if (Z_ISREF_P(data)) { ZEND_REF_ADD_TYPE_SOURCE(Z_REF_P(data), info); @@ -585,11 +615,20 @@ string_key: if (elements && *(*p-1) != ';' && *(*p-1) != '}') { (*p)--; - return 0; + goto failure; } } + if (var_hash) { + (*var_hash)->cur_depth--; + } return 1; + +failure: + if (var_hash) { + (*var_hash)->cur_depth--; + } + return 0; } static inline int finish_nested_data(UNSERIALIZE_PARAMETER) diff --git a/php.ini-development b/php.ini-development index 88b7a0b661..b81f1a04ca 100644 --- a/php.ini-development +++ b/php.ini-development @@ -284,6 +284,13 @@ implicit_flush = Off ; callback-function. unserialize_callback_func = +; The unserialize_max_depth specifies the default depth limit for unserialized +; structures. Setting the depth limit too high may result in stack overflows +; during unserialization. The unserialize_max_depth ini setting can be +; overridden by the max_depth option on individual unserialize() calls. +; A value of 0 disables the depth limit. +;unserialize_max_depth = 4096 + ; When floats & doubles are serialized, store serialize_precision significant ; digits after the floating point. The default value ensures that when floats ; are decoded with unserialize, the data will remain the same. diff --git a/php.ini-production b/php.ini-production index 3fb7d9201f..2cb2350547 100644 --- a/php.ini-production +++ b/php.ini-production @@ -284,6 +284,13 @@ implicit_flush = Off ; callback-function. unserialize_callback_func = +; The unserialize_max_depth specifies the default depth limit for unserialized +; structures. Setting the depth limit too high may result in stack overflows +; during unserialization. The unserialize_max_depth ini setting can be +; overridden by the max_depth option on individual unserialize() calls. +; A value of 0 disables the depth limit. +;unserialize_max_depth = 4096 + ; When floats & doubles are serialized, store serialize_precision significant ; digits after the floating point. The default value ensures that when floats ; are decoded with unserialize, the data will remain the same. |