Skip to content
Open
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
48 changes: 44 additions & 4 deletions ext/standard/php_fopen_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)))) {
Expand Down Expand Up @@ -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);
}
Expand All @@ -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 */
Expand Down
109 changes: 109 additions & 0 deletions ext/standard/tests/filters/max_filter_chain.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
--TEST--
At most 16 filters can be chained in one stream
--EXTENSIONS--
filter
--FILE--
<?php

function createFilterChains($n, $resource) {
$filter = 'string.toupper';
$pipes = 'php://filter/' . implode('|', array_fill(0, $n, $filter)) . "/resource=$resource";
$slashes = 'php://filter/' . implode('/', array_fill(0, $n, $filter)) . "/resource=$resource";
$resources = str_repeat("php://filter/$filter/resource=", $n) . $resource;
return [$pipes, $slashes, $resources];
}

$allowed_read = createFilterChains(16, 'data:text/plain,sixteen');
foreach ($allowed_read as $chain) {
var_dump(file_get_contents($chain));
}

$allowed_include = createFilterChains(16, 'php://temp');
foreach ($allowed_include as $chain) {
var_dump(include $chain);
}

$blocked_read = createFilterChains(17, 'data:text/plain,seventeen');
foreach ($blocked_read as $chain) {
var_dump(file_get_contents($chain));
}

$blocked_include = createFilterChains(17, 'php://temp');
foreach ($blocked_include as $chain) {
var_dump(include $chain);
}

$ctx = stream_context_create(['filter' => ['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"
14 changes: 14 additions & 0 deletions main/streams/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions main/streams/php_stream_filter_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading