Skip to content

Conversation

bwoebi
Copy link
Member

@bwoebi bwoebi commented Dec 30, 2015

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

if (Z_REFCOUNTED_P(zvalue)) {
Z_DELREF_P(zvalue);
}
zval_dtor(zvalue);
Copy link
Member

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--
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 definitely go to json or there need to be at least skipif json ext is not there

}
zend_string_release(key);

if (EG(exception)) {
parser->scanner.errcode = PHP_JSON_ERROR_DEPTH;
Copy link
Member

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.

Copy link
Member Author

@bwoebi bwoebi Sep 22, 2016

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.

Copy link
Member

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... :)

@bwoebi
Copy link
Member Author

bwoebi commented Sep 22, 2016

@bukka I'm planning to update the patch soon [It's not perfect yet, it was more of a PoC back then].

@bwoebi bwoebi force-pushed the collisionCount branch 6 times, most recently from 4cbb283 to 5042212 Compare September 25, 2016 19:24
@bwoebi
Copy link
Member Author

bwoebi commented Sep 25, 2016

The updated patch also includes support for SOAP, XML and databases.

Also applies suggestions by @bukka.

@nikic
Copy link
Member

nikic commented Sep 29, 2016

Did you already check the perf impact?

@bwoebi
Copy link
Member Author

bwoebi commented Sep 29, 2016

@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.

@nikic
Copy link
Member

nikic commented Sep 29, 2016

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.

Copy link
Member

@nikic nikic left a 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
Copy link
Member

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);
Copy link
Member

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);
Copy link
Member

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?

Copy link
Member

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

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

@bwoebi bwoebi Sep 29, 2016

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;
}
Copy link
Member

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?

Copy link
Member Author

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.

Copy link
Member

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.

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);
Copy link
Member

Choose a reason for hiding this comment

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

zval_add_ref(zv);
zval zv;
ZVAL_COPY_VALUE(&zv, src_entry);
zval_add_ref(&zv);
Copy link
Member

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.

Copy link
Member Author

@bwoebi bwoebi Sep 29, 2016

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...?!)

Copy link
Member

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);
Copy link
Member

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();
}
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

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)

Copy link
Member Author

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?

Copy link
Member

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.

Copy link
Member Author

@bwoebi bwoebi Sep 29, 2016

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);
Copy link
Member

Choose a reason for hiding this comment

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

element is used below.

@@ -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)) {
Copy link
Member

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?

@bwoebi
Copy link
Member Author

bwoebi commented Sep 29, 2016

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...

@bwoebi bwoebi force-pushed the collisionCount branch 2 times, most recently from 2ea09f5 to 6e1a29a Compare September 29, 2016 23:18
@nikic
Copy link
Member

nikic commented Jan 6, 2017

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).

@nikic nikic removed their assignment Jan 9, 2017
@php-pulls
Copy link

Comment on behalf of kalle at php.net:

Closing due to inactivity

@php-pulls php-pulls closed this Sep 26, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants