Skip to content
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
14 changes: 14 additions & 0 deletions Zend/tests/bug70644.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
--TEST--
Bug #70644: trivial hash complexity DoS attack (plain array)
--FILE--
<?php

$a = [];
$s = 1 << 16;
for ($i = 0; count($a) < $s; $i += $s) {
$a[$i] = 0;
}

?>
--EXPECTF--
Fatal error: Too many collisions in hashtable in %s on line %d
16 changes: 16 additions & 0 deletions Zend/tests/bug70644_2.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
--TEST--
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should go to ext/json/tests and have skip if json ext is not loaded.

Bug #70644: trivial hash complexity DoS attack (json_decode)
--FILE--
<?php

$s = 1 << 16;
$a = [];
for ($i = 0, $j = 0; $i < $s; $i++, $j += $s) {
$a[] = '"' . $j . '": null';
}
$j = '{' . implode(', ', $a) . '}';
var_dump(json_decode($j, true));

?>
--EXPECTF--
Fatal error: Too many collisions in hashtable in %s on line %d
26 changes: 18 additions & 8 deletions Zend/zend_execute.c
Original file line number Diff line number Diff line change
Expand Up @@ -1569,6 +1569,10 @@ static zend_always_inline zval *zend_fetch_dimension_address_inner(HashTable *ht
if (EXPECTED(Z_TYPE_P(dim) == IS_LONG)) {
hval = Z_LVAL_P(dim);
num_index:
if (type == BP_VAR_W) {
return zend_hash_index_add_or_return(ht, hval, &EG(uninitialized_zval));
}

retval = zend_hash_index_find(ht, hval);
if (retval == NULL) {
switch (type) {
Expand All @@ -1583,9 +1587,7 @@ static zend_always_inline zval *zend_fetch_dimension_address_inner(HashTable *ht
zend_error(E_NOTICE,"Undefined offset: " ZEND_LONG_FMT, hval);
retval = zend_hash_index_update(ht, hval, &EG(uninitialized_zval));
break;
case BP_VAR_W:
retval = zend_hash_index_add_new(ht, hval, &EG(uninitialized_zval));
break;
EMPTY_SWITCH_DEFAULT_CASE()
}
}
} else if (EXPECTED(Z_TYPE_P(dim) == IS_STRING)) {
Expand All @@ -1596,6 +1598,17 @@ static zend_always_inline zval *zend_fetch_dimension_address_inner(HashTable *ht
}
}
str_index:
if (type == BP_VAR_W) {
retval = zend_hash_add_or_return(ht, offset_key, &EG(uninitialized_zval));
if (UNEXPECTED(Z_TYPE_P(retval) == IS_INDIRECT)) {
retval = Z_INDIRECT_P(retval);
if (UNEXPECTED(Z_TYPE_P(retval) == IS_UNDEF)) {
ZVAL_NULL(retval);
}
}
return retval;
}

retval = zend_hash_find(ht, offset_key);
if (retval) {
/* support for $GLOBALS[...] */
Expand All @@ -1612,10 +1625,9 @@ static zend_always_inline zval *zend_fetch_dimension_address_inner(HashTable *ht
break;
case BP_VAR_RW:
zend_error(E_NOTICE,"Undefined index: %s", ZSTR_VAL(offset_key));
/* break missing intentionally */
case BP_VAR_W:
ZVAL_NULL(retval);
break;
EMPTY_SWITCH_DEFAULT_CASE()
}
}
}
Expand All @@ -1632,9 +1644,7 @@ static zend_always_inline zval *zend_fetch_dimension_address_inner(HashTable *ht
zend_error(E_NOTICE,"Undefined index: %s", ZSTR_VAL(offset_key));
retval = zend_hash_update(ht, offset_key, &EG(uninitialized_zval));
break;
case BP_VAR_W:
retval = zend_hash_add_new(ht, offset_key, &EG(uninitialized_zval));
break;
EMPTY_SWITCH_DEFAULT_CASE()
}
}
} else {
Expand Down
71 changes: 59 additions & 12 deletions Zend/zend_hash.c
Original file line number Diff line number Diff line change
Expand Up @@ -459,12 +459,25 @@ ZEND_API void ZEND_FASTCALL _zend_hash_iterators_update(HashTable *ht, HashPosit
}
}

static zend_always_inline Bucket *zend_hash_find_bucket(const HashTable *ht, zend_string *key)
/* To protect against HashDos attacks, we count collisions during the "find
* existing bucket with this key" phase of insertion operations. The check
* against MAX_COLLISIONS is only performed if *no* matching bucket is found,
* as that corresponds to insert operations (as opposed to updates).
*
* An important caveat of the implementation is that collisions will *not*
* be counted for add_new operations. find+add_new operations should be replaced
* with add_or_return operations.
*/
#define HT_MAX_COLLISIONS 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if a developer runs into this issue with an application without a malicious intention? Should this maybe a ini setting?

Or is this case really absolutely hypothetical (I don't have a in-depth understanding of the possibility of this happening outside actual DOS attempts) ?

Thank you!

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also think this should become an optional configuration.Hard encoding for 1000 may result in special needs can not be met

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mfn : it is.

Ini setting is bad, because relying on the user for such a crucial, misunderstood concept, is bad.
Like we hardcoded many internal-related things, GC_ROOT_BUFFER_MAX_ENTRIES f.e.


static zend_always_inline Bucket *zend_hash_find_bucket(
const HashTable *ht, zend_string *key, zend_bool for_insert)
{
zend_ulong h;
uint32_t nIndex;
uint32_t idx;
Bucket *p, *arData;
uint32_t num_collisions = 0;

h = zend_string_hash_val(key);
arData = ht->arData;
Expand All @@ -481,15 +494,21 @@ static zend_always_inline Bucket *zend_hash_find_bucket(const HashTable *ht, zen
return p;
}
idx = Z_NEXT(p->val);
num_collisions++;
}
if (for_insert && UNEXPECTED(num_collisions > HT_MAX_COLLISIONS)) {
zend_error_noreturn(E_ERROR, "Too many collisions in hashtable");
}
return NULL;
}

static zend_always_inline Bucket *zend_hash_str_find_bucket(const HashTable *ht, const char *str, size_t len, zend_ulong h)
static zend_always_inline Bucket *zend_hash_str_find_bucket(
const HashTable *ht, const char *str, size_t len, zend_ulong h, zend_bool for_insert)
{
uint32_t nIndex;
uint32_t idx;
Bucket *p, *arData;
uint32_t num_collisions = 0;

arData = ht->arData;
nIndex = h | ht->nTableMask;
Expand All @@ -504,15 +523,21 @@ static zend_always_inline Bucket *zend_hash_str_find_bucket(const HashTable *ht,
return p;
}
idx = Z_NEXT(p->val);
num_collisions++;
}
if (for_insert && UNEXPECTED(num_collisions > HT_MAX_COLLISIONS)) {
zend_error_noreturn(E_ERROR, "Too many collisions in hashtable");
}
return NULL;
}

static zend_always_inline Bucket *zend_hash_index_find_bucket(const HashTable *ht, zend_ulong h)
static zend_always_inline Bucket *zend_hash_index_find_bucket(
const HashTable *ht, zend_ulong h, zend_bool for_insert)
{
uint32_t nIndex;
uint32_t idx;
Bucket *p, *arData;
uint32_t num_collisions = 0;

arData = ht->arData;
nIndex = h | ht->nTableMask;
Expand All @@ -524,6 +549,10 @@ static zend_always_inline Bucket *zend_hash_index_find_bucket(const HashTable *h
return p;
}
idx = Z_NEXT(p->val);
num_collisions++;
}
if (for_insert && UNEXPECTED(num_collisions > HT_MAX_COLLISIONS)) {
zend_error_noreturn(E_ERROR, "Too many collisions in hashtable");
}
return NULL;
}
Expand All @@ -544,14 +573,17 @@ static zend_always_inline zval *_zend_hash_add_or_update_i(HashTable *ht, zend_s
} else if (ht->u.flags & HASH_FLAG_PACKED) {
zend_hash_packed_to_hash(ht);
} else if ((flag & HASH_ADD_NEW) == 0) {
p = zend_hash_find_bucket(ht, key);
p = zend_hash_find_bucket(ht, key, 1);

if (p) {
zval *data;

if (flag & HASH_ADD) {
return NULL;
}
if (flag & HASH_ADD_OR_RETURN) {
return &p->val;
}
ZEND_ASSERT(&p->val != pData);
data = &p->val;
if ((flag & HASH_UPDATE_INDIRECT) && Z_TYPE_P(data) == IS_INDIRECT) {
Expand Down Expand Up @@ -619,6 +651,11 @@ ZEND_API zval* ZEND_FASTCALL _zend_hash_add_new(HashTable *ht, zend_string *key,
return _zend_hash_add_or_update_i(ht, key, pData, HASH_ADD_NEW ZEND_FILE_LINE_RELAY_CC);
}

ZEND_API zval* ZEND_FASTCALL _zend_hash_add_or_return(HashTable *ht, zend_string *key, zval *pData ZEND_FILE_LINE_DC)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rename zend_hash_add_or_return() into zend_hash_lookup()

{
return _zend_hash_add_or_update_i(ht, key, pData, HASH_ADD_OR_RETURN ZEND_FILE_LINE_RELAY_CC);
}

ZEND_API zval* ZEND_FASTCALL _zend_hash_str_add_or_update(HashTable *ht, const char *str, size_t len, zval *pData, uint32_t flag ZEND_FILE_LINE_DC)
{
zend_string *key = zend_string_init(str, len, ht->u.flags & HASH_FLAG_PERSISTENT);
Expand Down Expand Up @@ -706,6 +743,9 @@ static zend_always_inline zval *_zend_hash_index_add_or_update_i(HashTable *ht,
if (flag & HASH_ADD) {
return NULL;
}
if (flag & HASH_ADD_OR_RETURN) {
return &p->val;
}
if (ht->pDestructor) {
ht->pDestructor(&p->val);
}
Expand Down Expand Up @@ -761,11 +801,14 @@ static zend_always_inline zval *_zend_hash_index_add_or_update_i(HashTable *ht,
convert_to_hash:
zend_hash_packed_to_hash(ht);
} else if ((flag & HASH_ADD_NEW) == 0) {
p = zend_hash_index_find_bucket(ht, h);
p = zend_hash_index_find_bucket(ht, h, 1);
if (p) {
if (flag & HASH_ADD) {
return NULL;
}
if (flag & HASH_ADD_OR_RETURN) {
return &p->val;
}
ZEND_ASSERT(&p->val != pData);
HANDLE_BLOCK_INTERRUPTIONS();
if (ht->pDestructor) {
Expand Down Expand Up @@ -820,6 +863,11 @@ ZEND_API zval* ZEND_FASTCALL _zend_hash_index_add_new(HashTable *ht, zend_ulong
return _zend_hash_index_add_or_update_i(ht, h, pData, HASH_ADD | HASH_ADD_NEW ZEND_FILE_LINE_RELAY_CC);
}

ZEND_API zval* ZEND_FASTCALL _zend_hash_index_add_or_return(HashTable *ht, zend_ulong h, zval *pData ZEND_FILE_LINE_DC)
{
return _zend_hash_index_add_or_update_i(ht, h, pData, HASH_ADD_OR_RETURN ZEND_FILE_LINE_RELAY_CC);
}

ZEND_API zval* ZEND_FASTCALL _zend_hash_index_update(HashTable *ht, zend_ulong h, zval *pData ZEND_FILE_LINE_DC)
{
return _zend_hash_index_add_or_update_i(ht, h, pData, HASH_UPDATE ZEND_FILE_LINE_RELAY_CC);
Expand Down Expand Up @@ -1923,7 +1971,7 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *ke

IS_CONSISTENT(ht);

p = zend_hash_find_bucket(ht, key);
p = zend_hash_find_bucket(ht, key, 0);
return p ? &p->val : NULL;
}

Expand All @@ -1935,7 +1983,7 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_str_find(const HashTable *ht, const char
IS_CONSISTENT(ht);

h = zend_inline_hash_func(str, len);
p = zend_hash_str_find_bucket(ht, str, len, h);
p = zend_hash_str_find_bucket(ht, str, len, h, 0);
return p ? &p->val : NULL;
}

Expand All @@ -1945,7 +1993,7 @@ ZEND_API zend_bool ZEND_FASTCALL zend_hash_exists(const HashTable *ht, zend_stri

IS_CONSISTENT(ht);

p = zend_hash_find_bucket(ht, key);
p = zend_hash_find_bucket(ht, key, 0);
return p ? 1 : 0;
}

Expand All @@ -1957,7 +2005,7 @@ ZEND_API zend_bool ZEND_FASTCALL zend_hash_str_exists(const HashTable *ht, const
IS_CONSISTENT(ht);

h = zend_inline_hash_func(str, len);
p = zend_hash_str_find_bucket(ht, str, len, h);
p = zend_hash_str_find_bucket(ht, str, len, h, 0);
return p ? 1 : 0;
}

Expand All @@ -1977,11 +2025,10 @@ ZEND_API zval* ZEND_FASTCALL zend_hash_index_find(const HashTable *ht, zend_ulon
return NULL;
}

p = zend_hash_index_find_bucket(ht, h);
p = zend_hash_index_find_bucket(ht, h, 0);
return p ? &p->val : NULL;
}


ZEND_API zend_bool ZEND_FASTCALL zend_hash_index_exists(const HashTable *ht, zend_ulong h)
{
Bucket *p;
Expand All @@ -1997,7 +2044,7 @@ ZEND_API zend_bool ZEND_FASTCALL zend_hash_index_exists(const HashTable *ht, zen
return 0;
}

p = zend_hash_index_find_bucket(ht, h);
p = zend_hash_index_find_bucket(ht, h, 0);
return p ? 1 : 0;
}

Expand Down
9 changes: 8 additions & 1 deletion Zend/zend_hash.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#define HASH_UPDATE_INDIRECT (1<<2)
#define HASH_ADD_NEW (1<<3)
#define HASH_ADD_NEXT (1<<4)
#define HASH_ADD_OR_RETURN (1<<5)

#define HASH_FLAG_PERSISTENT (1<<0)
#define HASH_FLAG_APPLY_PROTECTION (1<<1)
Expand Down Expand Up @@ -72,6 +73,7 @@ ZEND_API zval* ZEND_FASTCALL _zend_hash_update(HashTable *ht, zend_string *key,z
ZEND_API zval* ZEND_FASTCALL _zend_hash_update_ind(HashTable *ht, zend_string *key,zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_add(HashTable *ht, zend_string *key,zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_add_new(HashTable *ht, zend_string *key,zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_add_or_return(HashTable *ht, zend_string *key, zval *pData ZEND_FILE_LINE_DC);

#define zend_hash_update(ht, key, pData) \
_zend_hash_update(ht, key, pData ZEND_FILE_LINE_CC)
Expand All @@ -81,6 +83,8 @@ ZEND_API zval* ZEND_FASTCALL _zend_hash_add_new(HashTable *ht, zend_string *key,
_zend_hash_add(ht, key, pData ZEND_FILE_LINE_CC)
#define zend_hash_add_new(ht, key, pData) \
_zend_hash_add_new(ht, key, pData ZEND_FILE_LINE_CC)
#define zend_hash_add_or_return(ht, key, pData) \
_zend_hash_add_or_return(ht, key, pData ZEND_FILE_LINE_CC)

ZEND_API zval* ZEND_FASTCALL _zend_hash_str_add_or_update(HashTable *ht, const char *key, size_t len, zval *pData, uint32_t flag ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_str_update(HashTable *ht, const char *key, size_t len, zval *pData ZEND_FILE_LINE_DC);
Expand All @@ -100,6 +104,7 @@ ZEND_API zval* ZEND_FASTCALL _zend_hash_str_add_new(HashTable *ht, const char *k
ZEND_API zval* ZEND_FASTCALL _zend_hash_index_add_or_update(HashTable *ht, zend_ulong h, zval *pData, uint32_t flag ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_index_add(HashTable *ht, zend_ulong h, zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_index_add_new(HashTable *ht, zend_ulong h, zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_index_add_or_return(HashTable *ht, zend_ulong h, zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_index_update(HashTable *ht, zend_ulong h, zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_next_index_insert(HashTable *ht, zval *pData ZEND_FILE_LINE_DC);
ZEND_API zval* ZEND_FASTCALL _zend_hash_next_index_insert_new(HashTable *ht, zval *pData ZEND_FILE_LINE_DC);
Expand All @@ -108,6 +113,8 @@ ZEND_API zval* ZEND_FASTCALL _zend_hash_next_index_insert_new(HashTable *ht, zva
_zend_hash_index_add(ht, h, pData ZEND_FILE_LINE_CC)
#define zend_hash_index_add_new(ht, h, pData) \
_zend_hash_index_add_new(ht, h, pData ZEND_FILE_LINE_CC)
#define zend_hash_index_add_or_return(ht, h, pData) \
_zend_hash_index_add_or_return(ht, h, pData ZEND_FILE_LINE_CC)
#define zend_hash_index_update(ht, h, pData) \
_zend_hash_index_update(ht, h, pData ZEND_FILE_LINE_CC)
#define zend_hash_next_index_insert(ht, pData) \
Expand Down Expand Up @@ -150,7 +157,7 @@ ZEND_API int ZEND_FASTCALL zend_hash_str_del_ind(HashTable *ht, const char *key,
ZEND_API int ZEND_FASTCALL zend_hash_index_del(HashTable *ht, zend_ulong h);
ZEND_API void ZEND_FASTCALL zend_hash_del_bucket(HashTable *ht, Bucket *p);

/* Data retreival */
/* Data retrieval */
ZEND_API zval* ZEND_FASTCALL zend_hash_find(const HashTable *ht, zend_string *key);
ZEND_API zval* ZEND_FASTCALL zend_hash_str_find(const HashTable *ht, const char *key, size_t len);
ZEND_API zval* ZEND_FASTCALL zend_hash_index_find(const HashTable *ht, zend_ulong h);
Expand Down
Loading