diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c index 3f062bf2ea1e..b4c096b1ca26 100644 --- a/ext/standard/php_fopen_wrapper.c +++ b/ext/standard/php_fopen_wrapper.c @@ -144,13 +144,38 @@ 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_result php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain, php_stream_context *context) /* {{{ */ { char *p, *token = NULL; php_stream_filter *temp_filter; + zend_long max_filter_count = max_filter_count_default; + bool max_filter_count_configured = false; + if (context != NULL) { + zval *option_val = php_stream_context_get_option(context, "filter", "max_filter_count"); + if (option_val) { + max_filter_count = zval_get_long(option_val); + max_filter_count_configured = true; + } + } + p = php_strtok_r(filterlist, "|", &token); while (p) { + zend_long read_count = read_chain ? stream->readfilters.num_filters : 0; + zend_long write_count = write_chain ? stream->writefilters.num_filters : 0; + + if (read_count == max_filter_count || write_count == max_filter_count) { + if (max_filter_count_configured) { + return FAILURE; + } else { + // No max_filter_count configured; raise deprecation error if over default + zend_error(E_DEPRECATED, "Using more than " ZEND_LONG_FMT " 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)))) { @@ -172,6 +197,7 @@ static void php_stream_apply_filter_list(php_stream *stream, char *filterlist, i } p = php_strtok_r(NULL, "|", &token); } + return SUCCESS; } /* }}} */ @@ -370,17 +396,23 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c return NULL; } + zend_result safl_result = SUCCESS; *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); + safl_result = php_stream_apply_filter_list(stream, p + 5, 1, 0, context); } else if (!strncasecmp(p, "write=", 6)) { - php_stream_apply_filter_list(stream, p + 6, 0, 1); + safl_result = php_stream_apply_filter_list(stream, p + 6, 0, 1, context); } else { - php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE); + safl_result = php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE, context); } + + if (safl_result == FAILURE) { + break; + } + p = php_strtok_r(NULL, "/", &token); } efree(pathdup); @@ -390,6 +422,13 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c return NULL; } + if (safl_result == FAILURE) { + php_stream_wrapper_log_warn(wrapper, context, options, + PathTooLong, "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..a99ca6003e69 --- /dev/null +++ b/ext/standard/tests/filters/max_filter_chain.phpt @@ -0,0 +1,144 @@ +--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)); +} + +echo "# file_get_contents on 19 filters, max_filter_count=20\n"; +$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)); +} + +echo "# warning is only given once, even when we add two filters over the limit\n"; +$blocked_read = createFilterChains(18, 'data:text/plain,eighteen'); +foreach ($blocked_read as $chain) { + var_dump(file_get_contents($chain)); +} + +echo "# warn on too many write filters, even when number of read filters is OK\n"; +$filter = 'string.toupper'; +$write_filters = implode('|', array_fill(0, 16, $filter)); +$fp = fopen("php://filter/write=$write_filters/$filter/resource=php://temp", 'w+'); +var_dump(is_resource($fp)); + +echo "# setting max_filter_count to -1 disables warning\n"; +$ctx = stream_context_create(['filter' => ['max_filter_count' => -1]]); +$allowed_read = createFilterChains(20, 'data:text/plain,twenty'); +foreach ($allowed_read as $chain) { + var_dump(file_get_contents($chain, false, $ctx)); +} + +echo "# many filters with stream_filter_append still works\n"; +$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-- +# file_get_contents on 16 filters +string(7) "SIXTEEN" +string(7) "SIXTEEN" +string(7) "SIXTEEN" +# include on 16 filters +int(1) +int(1) +int(1) +# file_get_contents on 17 filters + +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" +# include on 17 filters + +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) +# file_get_contents on 3 filters, max_filter_count=2 + +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) +# file_get_contents on 19 filters, max_filter_count=20 +string(8) "NINETEEN" +string(8) "NINETEEN" +string(8) "NINETEEN" +# warning is only given once, even when we add two filters over the limit + +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" +# warn on too many write filters, even when number of read filters is OK + +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 +bool(true) +# setting max_filter_count to -1 disables warning +string(6) "TWENTY" +string(6) "TWENTY" +string(6) "TWENTY" +# many filters with stream_filter_append still works +string(20) "STREAM_FILTER_APPEND" diff --git a/main/streams/filter.c b/main/streams/filter.c index 35dbef455a30..ec7ed8602e64 100644 --- a/main/streams/filter.c +++ b/main/streams/filter.c @@ -340,6 +340,7 @@ PHPAPI void php_stream_filter_prepend_ex(php_stream_filter_chain *chain, php_str chain->tail = filter; } chain->head = filter; + chain->num_filters += 1; filter->chain = chain; } @@ -360,6 +361,7 @@ PHPAPI zend_result php_stream_filter_append_ex(php_stream_filter_chain *chain, p chain->head = filter; } chain->tail = filter; + chain->num_filters += 1; filter->chain = chain; if (&(stream->readfilters) == chain && (stream->writepos - stream->readpos) > 0) { @@ -445,6 +447,7 @@ PHPAPI void _php_stream_filter_append(php_stream_filter_chain *chain, php_stream filter->prev->next = NULL; chain->tail = filter->prev; } + chain->num_filters -= 1; } } @@ -546,6 +549,8 @@ PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bo filter->chain->tail = filter->prev; } + filter->chain->num_filters -= 1; + if (filter->res) { zend_list_delete(filter->res); } diff --git a/main/streams/php_stream_filter_api.h b/main/streams/php_stream_filter_api.h index 111127a8ad1a..c68660877be7 100644 --- a/main/streams/php_stream_filter_api.h +++ b/main/streams/php_stream_filter_api.h @@ -108,6 +108,7 @@ typedef struct _php_stream_filter_ops { typedef struct _php_stream_filter_chain { php_stream_filter *head, *tail; + zend_long num_filters; /* Owning stream */ php_stream *stream;