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
47 changes: 43 additions & 4 deletions ext/standard/php_fopen_wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)))) {
Expand All @@ -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;
}
/* }}} */

Expand Down Expand Up @@ -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);
Expand All @@ -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 */
Expand Down
144 changes: 144 additions & 0 deletions ext/standard/tests/filters/max_filter_chain.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
--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];
}

echo "# file_get_contents on 16 filters\n";
$allowed_read = createFilterChains(16, 'data:text/plain,sixteen');
Comment thread
Sjord marked this conversation as resolved.
foreach ($allowed_read as $chain) {
var_dump(file_get_contents($chain));
}

echo "# include on 16 filters\n";
$allowed_include = createFilterChains(16, 'php://temp');
foreach ($allowed_include as $chain) {
var_dump(include $chain);
}

echo "# file_get_contents on 17 filters\n";
$blocked_read = createFilterChains(17, 'data:text/plain,seventeen');
foreach ($blocked_read as $chain) {
var_dump(file_get_contents($chain));
}

echo "# include on 17 filters\n";
$blocked_include = createFilterChains(17, 'php://temp');
foreach ($blocked_include as $chain) {
var_dump(include $chain);
}

echo "# file_get_contents on 3 filters, max_filter_count=2\n";
$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));
}

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"
5 changes: 5 additions & 0 deletions main/streams/filter.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
}
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 @@ -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;
Expand Down
Loading