summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xext/standard/basic_functions.c1
-rw-r--r--ext/standard/basic_functions.h1
-rw-r--r--ext/standard/php_var.h5
-rw-r--r--ext/standard/tests/serialize/max_depth.phpt159
-rw-r--r--ext/standard/var.c53
-rw-r--r--ext/standard/var_unserializer.re53
-rw-r--r--php.ini-development7
-rw-r--r--php.ini-production7
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.