diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c index 89c2d9c49b0c..b53a7d80f42d 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/php_fopen_wrapper.c @@ -144,13 +144,45 @@ static const php_stream_ops php_stream_input_ops = { NULL /* set_option */ }; -static void php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain) /* {{{ */ +static const zend_long max_filter_count_default = 16; + +static zend_long php_get_max_filter_count(php_stream_context *context) { + if (context != NULL) { + zval *option_val = php_stream_context_get_option(context, "filter", "max_filter_count"); + if (option_val) { + zend_long custom_limit = zval_get_long(option_val); + if (custom_limit >= 0) { + return custom_limit; + } + } + } + return -1; +} + +static bool php_stream_has_too_many_filters(php_stream *stream, php_stream_context *context) { + zend_long max_filter_count = php_get_max_filter_count(context); + if (max_filter_count == -1) { + // If not explicitly configured we don't throw an error yet. + return false; + } + + zend_long count = MAX(php_stream_filter_count(&stream->readfilters), php_stream_filter_count(&stream->writefilters)); + return count > max_filter_count; +} + +static void php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain, bool warn_filter_count) /* {{{ */ { char *p, *token = NULL; php_stream_filter *temp_filter; p = php_strtok_r(filterlist, "|", &token); while (p) { + zend_long count = read_chain ? php_stream_filter_count(&stream->readfilters) : write_chain ? php_stream_filter_count(&stream->writefilters) : 0; + if (warn_filter_count && count == max_filter_count_default) { + zend_error(E_DEPRECATED, "Using more than %d filters in a php://filter URL is deprecated, " + "set this limit using the stream context option max_filter_count, or use stream_filter_append", max_filter_count_default); + } + php_url_decode(p, strlen(p)); if (read_chain) { if ((temp_filter = php_stream_filter_create(p, NULL, php_stream_is_persistent(stream)))) { @@ -355,16 +387,18 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c return NULL; } + bool max_filter_count_not_set = php_get_max_filter_count(context) == -1; + *p = '\0'; p = php_strtok_r(pathdup + 1, "/", &token); while (p) { if (!strncasecmp(p, "read=", 5)) { - php_stream_apply_filter_list(stream, p + 5, 1, 0); + php_stream_apply_filter_list(stream, p + 5, 1, 0, max_filter_count_not_set); } else if (!strncasecmp(p, "write=", 6)) { - php_stream_apply_filter_list(stream, p + 6, 0, 1); + php_stream_apply_filter_list(stream, p + 6, 0, 1, max_filter_count_not_set); } else { - php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE); + php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE, max_filter_count_not_set); } p = php_strtok_r(NULL, "/", &token); } @@ -375,6 +409,12 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c return NULL; } + if (php_stream_has_too_many_filters(stream, context)) { + php_stream_wrapper_log_error(wrapper, options, "too many filters"); + php_stream_close(stream); + return NULL; + } + return stream; } else { /* invalid php://thingy */ diff --git a/ext/standard/tests/filters/max_filter_chain.phpt b/ext/standard/tests/filters/max_filter_chain.phpt new file mode 100644 index 000000000000..520c84b167be --- /dev/null +++ b/ext/standard/tests/filters/max_filter_chain.phpt @@ -0,0 +1,109 @@ +--TEST-- +At most 16 filters can be chained in one stream +--EXTENSIONS-- +filter +--FILE-- + ['max_filter_count' => 2]]); +$blocked_read = createFilterChains(3, 'data:text/plain,three'); +foreach ($blocked_read as $chain) { + var_dump(file_get_contents($chain, false, $ctx)); +} + +$ctx = stream_context_create(['filter' => ['max_filter_count' => 20]]); +$allowed_read = createFilterChains(19, 'data:text/plain,nineteen'); +foreach ($allowed_read as $chain) { + var_dump(file_get_contents($chain, false, $ctx)); +} + +// Test that the warning is only given once, even when we add two filters over the limit. +$blocked_read = createFilterChains(18, 'data:text/plain,eighteen'); +foreach ($blocked_read as $chain) { + var_dump(file_get_contents($chain)); +} + +// many filters with stream_filter_append still works +$fp = fopen('data:text/plain,stream_filter_append', 'r'); +for ($i = 0; $i < 80; $i++) { + stream_filter_append($fp, 'string.toupper'); +} +var_dump(fread($fp, 30)); +fclose($fp); + +?> +--EXPECTF-- +string(7) "SIXTEEN" +string(7) "SIXTEEN" +string(7) "SIXTEEN" +int(1) +int(1) +int(1) + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +string(9) "SEVENTEEN" + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +string(9) "SEVENTEEN" + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +string(9) "SEVENTEEN" + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +int(1) + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +int(1) + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +int(1) + +Warning: file_get_contents(php://filter/string.toupper|string.toupper|string.toupper/resource=data:text/plain,three): Failed to open stream: too many filters in %s on line %d +bool(false) + +Warning: file_get_contents(php://filter/string.toupper/string.toupper/string.toupper/resource=data:text/plain,three): Failed to open stream: too many filters in %s on line %d +bool(false) + +Warning: file_get_contents(php://filter/string.toupper/resource=php://filter/string.toupper/resource=php://filter/string.toupper/resource=data:text/plain,three): Failed to open stream: too many filters in %s on line %d +bool(false) +string(8) "NINETEEN" +string(8) "NINETEEN" +string(8) "NINETEEN" + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +string(8) "EIGHTEEN" + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +string(8) "EIGHTEEN" + +Deprecated: Using more than 16 filters in a php://filter URL is deprecated, set this limit using the stream context option max_filter_count, or use stream_filter_append in %smax_filter_chain.php on line %d +string(8) "EIGHTEEN" +string(20) "STREAM_FILTER_APPEND" diff --git a/main/streams/filter.c b/main/streams/filter.c index 3a19f5ce918a..265b21be27e8 100644 --- a/main/streams/filter.c +++ b/main/streams/filter.c @@ -447,6 +447,20 @@ PHPAPI void _php_stream_filter_append(php_stream_filter_chain *chain, php_stream } } +PHPAPI zend_long php_stream_filter_count(php_stream_filter_chain *chain) { + if (chain->head == NULL) { + return 0; + } + + int count = 1; + php_stream_filter *node = chain->head; + while (node != chain->tail) { + count += 1; + node = node->next; + } + return count; +} + PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool finish) { php_stream_bucket_brigade brig_a = { NULL, NULL }, brig_b = { NULL, NULL }, *inp = &brig_a, *outp = &brig_b, *brig_temp; diff --git a/main/streams/php_stream_filter_api.h b/main/streams/php_stream_filter_api.h index 111127a8ad1a..612c71330051 100644 --- a/main/streams/php_stream_filter_api.h +++ b/main/streams/php_stream_filter_api.h @@ -138,6 +138,7 @@ PHPAPI void _php_stream_filter_prepend(php_stream_filter_chain *chain, php_strea PHPAPI void php_stream_filter_prepend_ex(php_stream_filter_chain *chain, php_stream_filter *filter); PHPAPI void _php_stream_filter_append(php_stream_filter_chain *chain, php_stream_filter *filter); PHPAPI zend_result php_stream_filter_append_ex(php_stream_filter_chain *chain, php_stream_filter *filter); +PHPAPI zend_long php_stream_filter_count(php_stream_filter_chain *chain); PHPAPI zend_result _php_stream_filter_flush(php_stream_filter *filter, bool finish); PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bool call_dtor); PHPAPI void php_stream_filter_free(php_stream_filter *filter);