Skip to content

Commit 04f3d28

Browse files
committed
ext/standard: reject a dechunk chunk size that overflows size_t
php_dechunk() accumulated the hex chunk size with chunk_size * 16 + digit and never checked the multiply, so a size of 2^64 wrapped to 0. A zero size reads as the terminating chunk, so the filter stopped there and dropped the body it had been handed. Error out instead, as the other malformed-size cases already do. Closes GH-22786
1 parent 754ea6e commit 04f3d28

2 files changed

Lines changed: 48 additions & 3 deletions

File tree

ext/standard/filters.c

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,19 +1715,26 @@ static size_t php_dechunk(char *buf, size_t len, php_chunked_filter_data *data)
17151715
data->chunk_size = 0;
17161716
case CHUNK_SIZE:
17171717
while (p < end) {
1718+
size_t digit;
1719+
17181720
if (*p >= '0' && *p <= '9') {
1719-
data->chunk_size = (data->chunk_size * 16) + (*p - '0');
1721+
digit = *p - '0';
17201722
} else if (*p >= 'A' && *p <= 'F') {
1721-
data->chunk_size = (data->chunk_size * 16) + (*p - 'A' + 10);
1723+
digit = *p - 'A' + 10;
17221724
} else if (*p >= 'a' && *p <= 'f') {
1723-
data->chunk_size = (data->chunk_size * 16) + (*p - 'a' + 10);
1725+
digit = *p - 'a' + 10;
17241726
} else if (data->state == CHUNK_SIZE_START) {
17251727
data->state = CHUNK_ERROR;
17261728
break;
17271729
} else {
17281730
data->state = CHUNK_SIZE_EXT;
17291731
break;
17301732
}
1733+
if (data->chunk_size > (SIZE_MAX / 16)) {
1734+
data->state = CHUNK_ERROR;
1735+
break;
1736+
}
1737+
data->chunk_size = (data->chunk_size * 16) + digit;
17311738
data->state = CHUNK_SIZE;
17321739
p++;
17331740
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
--TEST--
2+
dechunk filter must reject a chunk size that overflows size_t
3+
--SKIPIF--
4+
<?php
5+
$filters = stream_get_filters();
6+
if(! in_array( "dechunk", $filters )) die( "skip Chunked filter not available." );
7+
?>
8+
--INI--
9+
allow_url_fopen=1
10+
--FILE--
11+
<?php
12+
/* Both sizes exceed SIZE_MAX, so parsing stops and the rest is passed through
13+
raw. The guard trips at SIZE_MAX/16, so how many digits are consumed first
14+
follows the width of size_t: %s covers the leftover run. Unguarded, the
15+
first size wraps to 0, which reads as the terminating chunk and drops the
16+
body; the second wraps to SIZE_MAX and swallows the rest as one chunk. */
17+
$streams = [
18+
"data://text/plain,10000000000000000\nBODYDATA\n0\n",
19+
"data://text/plain,fffffffffffffffff\nBODYDATA\n0\n",
20+
"data://text/plain,5\nhello\n0\n",
21+
];
22+
foreach ($streams as $name) {
23+
$fp = fopen($name, "r");
24+
stream_filter_append($fp, "dechunk", STREAM_FILTER_READ);
25+
var_dump(stream_get_contents($fp));
26+
fclose($fp);
27+
}
28+
?>
29+
--EXPECTF--
30+
string(%d) "%s
31+
BODYDATA
32+
0
33+
"
34+
string(%d) "%s
35+
BODYDATA
36+
0
37+
"
38+
string(5) "hello"

0 commit comments

Comments
 (0)