Skip to content

Commit ed34ad4

Browse files
Sjordjvoisin
andauthored
ext/standard: limit maximum number of filter chains (#22110)
Limit php://filter URLs to 16 filters by default and allow applications to override the limit with filter.max_filter_count. RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains Co-authored-by: jvoisin <julien.voisin@dustri.org>
1 parent b74e0f7 commit ed34ad4

6 files changed

Lines changed: 207 additions & 4 deletions

File tree

NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ PHP NEWS
2121
ReflectionAttribute::getShortName(). (Girgias)
2222

2323
- Standard:
24+
. Added the "filter.max_filter_count" stream context option for php://filter
25+
URLs. Using more than 16 filters without configuring this option is now
26+
deprecated. (Sjoerd Langkemper)
2427
. The following functions now raise a ValueError when the $filename argument
2528
contains NUL bytes: fileperms(), fileinode(), filesize(), fileowner(),
2629
filegroup(), fileatime(), filemtime(), filectime(), filetype(),

UPGRADING

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,11 @@ PHP 8.6 UPGRADE NOTES
363363
internal API. It is controlled using error_mode, error_store and
364364
error_handler stream context options.
365365
RFC: https://wiki.php.net/rfc/stream_errors
366+
. Added the "filter.max_filter_count" stream context option for php://filter
367+
URLs. When set, opening the stream fails with a warning if the URL would
368+
add more filters than the configured value. Negative values disable the
369+
check.
370+
RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains
366371
. Added stream socket context option so_reuseaddr that allows disabling
367372
address reuse (SO_REUSEADDR) and explicitly uses SO_EXCLUSIVEADDRUSE on
368373
Windows.
@@ -413,6 +418,12 @@ PHP 8.6 UPGRADE NOTES
413418
is no longer maintained.
414419
RFC: https://wiki.php.net/rfc/eol-oniguruma
415420

421+
- Standard:
422+
. Using more than 16 filters in a php://filter URL without configuring the
423+
"filter.max_filter_count" stream context option now emits an E_DEPRECATED
424+
warning. Use stream_filter_append() or configure this option explicitly.
425+
RFC: https://wiki.php.net/rfc/limit-maximum-number-of-filter-chains
426+
416427
========================================
417428
5. Changed Functions
418429
========================================

ext/standard/php_fopen_wrapper.c

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,38 @@ static const php_stream_ops php_stream_input_ops = {
144144
NULL /* set_option */
145145
};
146146

147-
static void php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain) /* {{{ */
147+
static const zend_long max_filter_count_default = 16;
148+
149+
static zend_result php_stream_apply_filter_list(php_stream *stream, char *filterlist, int read_chain, int write_chain, php_stream_context *context) /* {{{ */
148150
{
149151
char *p, *token = NULL;
150152
php_stream_filter *temp_filter;
151153

154+
zend_long max_filter_count = max_filter_count_default;
155+
bool max_filter_count_configured = false;
156+
if (context != NULL) {
157+
zval *option_val = php_stream_context_get_option(context, "filter", "max_filter_count");
158+
if (option_val) {
159+
max_filter_count = zval_get_long(option_val);
160+
max_filter_count_configured = true;
161+
}
162+
}
163+
152164
p = php_strtok_r(filterlist, "|", &token);
153165
while (p) {
166+
zend_long read_count = read_chain ? stream->readfilters.num_filters : 0;
167+
zend_long write_count = write_chain ? stream->writefilters.num_filters : 0;
168+
169+
if (read_count == max_filter_count || write_count == max_filter_count) {
170+
if (max_filter_count_configured) {
171+
return FAILURE;
172+
} else {
173+
// No max_filter_count configured; raise deprecation error if over default
174+
zend_error(E_DEPRECATED, "Using more than " ZEND_LONG_FMT " filters in a php://filter URL is deprecated, "
175+
"set this limit using the stream context option max_filter_count, or use stream_filter_append", max_filter_count_default);
176+
}
177+
}
178+
154179
php_url_decode(p, strlen(p));
155180
if (read_chain) {
156181
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
172197
}
173198
p = php_strtok_r(NULL, "|", &token);
174199
}
200+
return SUCCESS;
175201
}
176202
/* }}} */
177203

@@ -362,17 +388,23 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c
362388
return NULL;
363389
}
364390

391+
zend_result safl_result = SUCCESS;
365392
*p = '\0';
366393

367394
p = php_strtok_r(pathdup + 1, "/", &token);
368395
while (p) {
369396
if (!strncasecmp(p, "read=", 5)) {
370-
php_stream_apply_filter_list(stream, p + 5, 1, 0);
397+
safl_result = php_stream_apply_filter_list(stream, p + 5, 1, 0, context);
371398
} else if (!strncasecmp(p, "write=", 6)) {
372-
php_stream_apply_filter_list(stream, p + 6, 0, 1);
399+
safl_result = php_stream_apply_filter_list(stream, p + 6, 0, 1, context);
373400
} else {
374-
php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE);
401+
safl_result = php_stream_apply_filter_list(stream, p, mode_rw & PHP_STREAM_FILTER_READ, mode_rw & PHP_STREAM_FILTER_WRITE, context);
375402
}
403+
404+
if (safl_result == FAILURE) {
405+
break;
406+
}
407+
376408
p = php_strtok_r(NULL, "/", &token);
377409
}
378410
efree(pathdup);
@@ -382,6 +414,13 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c
382414
return NULL;
383415
}
384416

417+
if (safl_result == FAILURE) {
418+
php_stream_wrapper_log_warn(wrapper, context, options,
419+
PathTooLong, "too many filters");
420+
php_stream_close(stream);
421+
return NULL;
422+
}
423+
385424
return stream;
386425
} else {
387426
/* invalid php://thingy */
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
--TEST--
2+
At most 16 filters can be chained in one stream
3+
--EXTENSIONS--
4+
filter
5+
--FILE--
6+
<?php
7+
8+
function createFilterChains($n, $resource) {
9+
$filter = 'string.toupper';
10+
$pipes = 'php://filter/' . implode('|', array_fill(0, $n, $filter)) . "/resource=$resource";
11+
$slashes = 'php://filter/' . implode('/', array_fill(0, $n, $filter)) . "/resource=$resource";
12+
$resources = str_repeat("php://filter/$filter/resource=", $n) . $resource;
13+
return [$pipes, $slashes, $resources];
14+
}
15+
16+
echo "# file_get_contents on 16 filters\n";
17+
$allowed_read = createFilterChains(16, 'data:text/plain,sixteen');
18+
foreach ($allowed_read as $chain) {
19+
var_dump(file_get_contents($chain));
20+
}
21+
22+
echo "# include on 16 filters\n";
23+
$allowed_include = createFilterChains(16, 'php://temp');
24+
foreach ($allowed_include as $chain) {
25+
var_dump(include $chain);
26+
}
27+
28+
echo "# file_get_contents on 17 filters\n";
29+
$blocked_read = createFilterChains(17, 'data:text/plain,seventeen');
30+
foreach ($blocked_read as $chain) {
31+
var_dump(file_get_contents($chain));
32+
}
33+
34+
echo "# include on 17 filters\n";
35+
$blocked_include = createFilterChains(17, 'php://temp');
36+
foreach ($blocked_include as $chain) {
37+
var_dump(include $chain);
38+
}
39+
40+
echo "# file_get_contents on 3 filters, max_filter_count=2\n";
41+
$ctx = stream_context_create(['filter' => ['max_filter_count' => 2]]);
42+
$blocked_read = createFilterChains(3, 'data:text/plain,three');
43+
foreach ($blocked_read as $chain) {
44+
var_dump(file_get_contents($chain, false, $ctx));
45+
}
46+
47+
echo "# file_get_contents on 19 filters, max_filter_count=20\n";
48+
$ctx = stream_context_create(['filter' => ['max_filter_count' => 20]]);
49+
$allowed_read = createFilterChains(19, 'data:text/plain,nineteen');
50+
foreach ($allowed_read as $chain) {
51+
var_dump(file_get_contents($chain, false, $ctx));
52+
}
53+
54+
echo "# warning is only given once, even when we add two filters over the limit\n";
55+
$blocked_read = createFilterChains(18, 'data:text/plain,eighteen');
56+
foreach ($blocked_read as $chain) {
57+
var_dump(file_get_contents($chain));
58+
}
59+
60+
echo "# warn on too many write filters, even when number of read filters is OK\n";
61+
$filter = 'string.toupper';
62+
$write_filters = implode('|', array_fill(0, 16, $filter));
63+
$fp = fopen("php://filter/write=$write_filters/$filter/resource=php://temp", 'w+');
64+
var_dump(is_resource($fp));
65+
66+
echo "# setting max_filter_count to -1 disables warning\n";
67+
$ctx = stream_context_create(['filter' => ['max_filter_count' => -1]]);
68+
$allowed_read = createFilterChains(20, 'data:text/plain,twenty');
69+
foreach ($allowed_read as $chain) {
70+
var_dump(file_get_contents($chain, false, $ctx));
71+
}
72+
73+
echo "# many filters with stream_filter_append still works\n";
74+
$fp = fopen('data:text/plain,stream_filter_append', 'r');
75+
for ($i = 0; $i < 80; $i++) {
76+
stream_filter_append($fp, 'string.toupper');
77+
}
78+
var_dump(fread($fp, 30));
79+
fclose($fp);
80+
81+
?>
82+
--EXPECTF--
83+
# file_get_contents on 16 filters
84+
string(7) "SIXTEEN"
85+
string(7) "SIXTEEN"
86+
string(7) "SIXTEEN"
87+
# include on 16 filters
88+
int(1)
89+
int(1)
90+
int(1)
91+
# file_get_contents on 17 filters
92+
93+
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
94+
string(9) "SEVENTEEN"
95+
96+
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
97+
string(9) "SEVENTEEN"
98+
99+
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
100+
string(9) "SEVENTEEN"
101+
# include on 17 filters
102+
103+
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
104+
int(1)
105+
106+
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
107+
int(1)
108+
109+
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
110+
int(1)
111+
# file_get_contents on 3 filters, max_filter_count=2
112+
113+
Warning: file_get_contents(): Failed to open stream: too many filters in %s on line %d
114+
bool(false)
115+
116+
Warning: file_get_contents(): Failed to open stream: too many filters in %s on line %d
117+
bool(false)
118+
119+
Warning: file_get_contents(): Failed to open stream: too many filters in %s on line %d
120+
bool(false)
121+
# file_get_contents on 19 filters, max_filter_count=20
122+
string(8) "NINETEEN"
123+
string(8) "NINETEEN"
124+
string(8) "NINETEEN"
125+
# warning is only given once, even when we add two filters over the limit
126+
127+
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
128+
string(8) "EIGHTEEN"
129+
130+
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
131+
string(8) "EIGHTEEN"
132+
133+
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
134+
string(8) "EIGHTEEN"
135+
# warn on too many write filters, even when number of read filters is OK
136+
137+
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
138+
bool(true)
139+
# setting max_filter_count to -1 disables warning
140+
string(6) "TWENTY"
141+
string(6) "TWENTY"
142+
string(6) "TWENTY"
143+
# many filters with stream_filter_append still works
144+
string(20) "STREAM_FILTER_APPEND"

main/streams/filter.c

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,7 @@ PHPAPI void php_stream_filter_prepend_ex(php_stream_filter_chain *chain, php_str
339339
chain->tail = filter;
340340
}
341341
chain->head = filter;
342+
chain->num_filters += 1;
342343
filter->chain = chain;
343344
}
344345

@@ -359,6 +360,7 @@ PHPAPI zend_result php_stream_filter_append_ex(php_stream_filter_chain *chain, p
359360
chain->head = filter;
360361
}
361362
chain->tail = filter;
363+
chain->num_filters += 1;
362364
filter->chain = chain;
363365

364366
if (&(stream->readfilters) == chain && (stream->writepos - stream->readpos) > 0) {
@@ -444,6 +446,7 @@ PHPAPI void php_stream_filter_append(php_stream_filter_chain *chain, php_stream_
444446
filter->prev->next = NULL;
445447
chain->tail = filter->prev;
446448
}
449+
chain->num_filters -= 1;
447450
}
448451
}
449452

@@ -545,6 +548,8 @@ PHPAPI php_stream_filter *php_stream_filter_remove(php_stream_filter *filter, bo
545548
filter->chain->tail = filter->prev;
546549
}
547550

551+
filter->chain->num_filters -= 1;
552+
548553
if (filter->res) {
549554
zend_list_delete(filter->res);
550555
}

main/streams/php_stream_filter_api.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ typedef struct _php_stream_filter_ops {
108108

109109
typedef struct _php_stream_filter_chain {
110110
php_stream_filter *head, *tail;
111+
zend_long num_filters;
111112

112113
/* Owning stream */
113114
php_stream *stream;

0 commit comments

Comments
 (0)