-
Notifications
You must be signed in to change notification settings - Fork 7.9k
Collision counting against HashDos attacks #1706
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
Conversation
ext/json/json_parser.y
Outdated
if (Z_REFCOUNTED_P(zvalue)) { | ||
Z_DELREF_P(zvalue); | ||
} | ||
zval_dtor(zvalue); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value is dtored later so we don't need to call ti now. The only thing that we need to do is Z_DELREF_P(zvalue)
because there is Z_ADDREF_P
. Technically zvalue will be always refcounted so we could just do Z_DELREF_P(zvalue)
but zval_dtor is unnecessary.
@@ -0,0 +1,18 @@ | |||
--TEST-- |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should definitely go to json or there need to be at least skipif json ext is not there
ext/json/json_parser.tab.c
Outdated
} | ||
zend_string_release(key); | ||
|
||
if (EG(exception)) { | ||
parser->scanner.errcode = PHP_JSON_ERROR_DEPTH; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking about it, I would actually go for a new json error as the depth is for something else so user knows that it failed because of the attack.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, adding a new JSON error isn't worth it, as, like Nikita says:
Getting C=1000
collisions for a single hash-value is extremely unlikely until you
explicitly design your keys to trigger this situation.
Thus it's just too unlikely to ever be helpful in legitimate situations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that it would be useful for users that want to identify that there is some attempt to attack their app. Lets say you have got a public API and you would like to identify that someone is sending such json (possibly trying to dos php version without this patch), then you might want to like to possibly log such case specially. That was my thinking when I suggested a new error... :)
@bukka I'm planning to update the patch soon [It's not perfect yet, it was more of a PoC back then]. |
4cbb283
to
5042212
Compare
The updated patch also includes support for SOAP, XML and databases. Also applies suggestions by @bukka. |
Did you already check the perf impact? |
@nikic Last time I checked (back then in January), the difference was well within margin of error upon running it against wordpress. There should not be any difference in that result now. |
It would be good to benchmark individual array operations as well. IIRC WordPress results stayed the same even after switching to SipHash, while individual operations regressed a lot. Both numbers are important. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My main overall concern is that there is a lot of places where the _exception
variant is used without explicitly checking the exception condition. In some cases it will even return a success condition from the function in this case. I'm sure this is going to be fine in most cases, but I don't want to review what the exact effect of throwing an exception somewhere deep in mysqlnd is, while also returning a success value.
I'd like to either have additional test coverage, or drop the exception changes where test coverage cannot be easily obtained. Many of those seem rather tenuous anyway, such as column names in database results (generally not attacker controlled).
?> | ||
--EXPECTF-- | ||
|
||
Fatal error: Uncaught HashCollisionError: Too many collisions in array in %s:%d |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For tests, I'd suggest to always catch the HashCollisionError to make sure that there are no leaks.
} else if (EXPECTED(Z_TYPE_P(offset) == IS_LONG)) { | ||
hval = Z_LVAL_P(offset); | ||
ZEND_VM_C_LABEL(num_index): | ||
zend_hash_index_update(Z_ARRVAL_P(EX_VAR(opline->result.var)), hval, expr_ptr); | ||
zend_hash_index_update_exception(Z_ARRVAL_P(EX_VAR(opline->result.var)), hval, expr_ptr); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A test:
<?php
$elems = '';
for ($i = 0; $i < 2048; $i++) {
$elems .= $i * (1<<16) . " => 0, ";
}
$code = <<<PHP
\$x = 0;
return [\$x, $elems];
PHP;
try {
eval($code);
} catch (HashCollisionError $e) {
echo $e->getMessage(), "\n";
}
add_assoc_string(&tmpi, name, val); | ||
zval zv; | ||
ZVAL_STRING(&zv, val); | ||
zend_hash_str_update_exception(Z_ARRVAL(tmpi), name, strlen(name), &zv); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the add_assoc_stringl
for the cases above?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or not only above for that matter
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are not taking user-input, hence I couldn't be bothered; I may still add it if you really like to?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To clarify, are you saying that this is the only branch that takes user-input? Not being familiar with exif, they all look the same to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I followed it through source code; it's the only branch.
zval_dtor(object); | ||
zend_clear_exception(); | ||
return FAILURE; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like the exception only covers the array case. Wouldn't it still be possible to trigger a fatal error if decoding is done into objects?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
zend_std_write_property should take care of that itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I missed that change.
ext/mysqlnd/mysqlnd_statistics.c
Outdated
zend_string *name = zend_string_init(names[i].s, names[i].l, 0); | ||
ZVAL_STR(&zv, zend_strpprintf(0, MYSQLND_LLU_SPEC, stats->values[i])); | ||
zend_hash_update_exception(Z_ARRVAL_P(return_value), name, &zv); | ||
zend_string_release(name); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The names used here seem to be hardcoded: https://github.com/bwoebi/php-src/blob/5042212ef50143803262bbeff1a2b3e91867ce5d/ext/mysqlnd/mysqlnd_statistics.c#L29
zval_add_ref(zv); | ||
zval zv; | ||
ZVAL_COPY_VALUE(&zv, src_entry); | ||
zval_add_ref(&zv); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add_ref needs to be called after insertion, otherwise it will not behave correctly. You can replace it with some manual rc1 ref deref code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are you sure? Note that I'm operating on a value copy here. (Yes, zval_add_ref(src_entry)
would break, that's right. But now it's on a copy...?!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, it's fine.
@@ -371,7 +371,7 @@ PHP_FUNCTION(iptcparse) | |||
if ((element = zend_hash_str_find(Z_ARRVAL_P(return_value), key, strlen(key))) == NULL) { | |||
array_init(&values); | |||
|
|||
element = zend_hash_str_update(Z_ARRVAL_P(return_value), key, strlen(key), &values); | |||
element = zend_hash_str_update_exception(Z_ARRVAL_P(return_value), key, strlen(key), &values); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The return value is used below.
/* parse_str() tries to parse as much as possible (per treat_data behavior), hence we just ignore possible HashCollisionErrors */ | ||
if (EG(exception) && EG(exception)->ce == zend_ce_hash_collision_error) { | ||
zend_clear_exception(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I get why this needs special handling. Imho this should throw like everything else.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When max_input_vars is exceeded, only a warning is emitted and no exceptions either.
Perhaps we should make this function emit a warning in this case too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe. I'd also like to know what currently happens with this exception if it occurs during request parsing (i.e. max_input_vars > collision limit)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure how exactly I'd phpt that? It would somehow involve using CGI? Do you know how that works?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a quick look tests/basic/027.phpt might help.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good, added a test tests/basic/bug70644.phpt … it is just fatal-ing here:
Fatal error: Too many collisions in array in Unknown on line 0
which is fine.
@@ -692,7 +692,7 @@ static void _xml_add_to_info(xml_parser *parser,char *name) | |||
if ((element = zend_hash_str_find(Z_ARRVAL(parser->info), name, strlen(name))) == NULL) { | |||
zval values; | |||
array_init(&values); | |||
element = zend_hash_str_update(Z_ARRVAL(parser->info), name, strlen(name), &values); | |||
element = zend_hash_str_update_exception(Z_ARRVAL(parser->info), name, strlen(name), &values); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
element
is used below.
ext/xmlrpc/xmlrpc-epi-php.c
Outdated
@@ -1292,7 +1292,7 @@ int set_zval_xmlrpc_type(zval* value, XMLRPC_VALUE_TYPE newtype) /* {{{ */ | |||
ZVAL_LONG(&ztimestamp, timestamp); | |||
|
|||
convert_to_object(value); | |||
if (zend_hash_str_update(Z_OBJPROP_P(value), OBJECT_TYPE_ATTR, sizeof(OBJECT_TYPE_ATTR) - 1, &type)) { | |||
if (zend_hash_str_update_exception(Z_OBJPROP_P(value), OBJECT_TYPE_ATTR, sizeof(OBJECT_TYPE_ATTR) - 1, &type)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will treat reaching the collision limit as a success condition. Intentional?
5042212
to
5103d9e
Compare
There are things like iptc, xmlrpc or mysql protocol etc. where I'd have to manually craft an example - I lack the knowledge of the exact formats and don't really want to bother reading it up in detail... Thus there will be no tests unless someone else provides these... |
2ea09f5
to
6e1a29a
Compare
6e1a29a
to
2d6c5cb
Compare
As mentioned on the other PR, we didn't get a consensus on internals about this change, so probably this is going to need an RFC to go forward. See also #2149 for an alternative approach (with impractical perf impact). |
Comment on behalf of kalle at php.net: Closing due to inactivity |
This PR supersedes #1565.
It adds graceful handling with exceptions on necessary places:
array_combine, array_column, array_flip, array_merge(_recursive) and the trivial array modification (
$a["key"] = $val;
)and general graceful handling for:
parse_str and json_decode