Skip to content

Commit abb0784

Browse files
committed
zlib/bz2/phar: cap decompression output at filter level
Adds an optional max_output parameter to zlib.inflate and bzip2.decompress filters. When set, the filter tracks bytes emitted and aborts with PSFS_ERR_FATAL once the cap is exceeded, stopping decompression amplification mid-stream instead of after the full payload has landed on disk. phar_open_entry_fp() passes entry->uncompressed_filesize as the cap so a lying phar (small declared size, payload that decompresses beyond it) fails during streaming. The previous post-copy filesize mismatch check is retained to catch under-decompression. The parameter is opt-in: omitting the key keeps existing behavior for all current callers.
1 parent f7eb5ef commit abb0784

File tree

7 files changed

+205
-4
lines changed

7 files changed

+205
-4
lines changed

ext/bz2/bz2_filter.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ typedef struct _php_bz2_filter_data {
3535
char *outbuf;
3636
size_t inbuf_len;
3737
size_t outbuf_len;
38+
size_t max_output;
39+
size_t total_output;
3840

3941
enum strm_status status; /* Decompress option */
4042
unsigned int small_footprint : 1; /* Decompress option */
@@ -137,6 +139,12 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
137139
if (data->strm.avail_out < data->outbuf_len) {
138140
php_stream_bucket *out_bucket;
139141
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
142+
data->total_output += bucketlen;
143+
if (data->max_output && data->total_output > data->max_output) {
144+
php_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output");
145+
php_stream_bucket_delref(bucket);
146+
return PSFS_ERR_FATAL;
147+
}
140148
out_bucket = php_stream_bucket_new(stream, estrndup(data->outbuf, bucketlen), bucketlen, 1, 0);
141149
php_stream_bucket_append(buckets_out, out_bucket);
142150
data->strm.avail_out = data->outbuf_len;
@@ -160,6 +168,11 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
160168
if (data->strm.avail_out < data->outbuf_len) {
161169
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
162170

171+
data->total_output += bucketlen;
172+
if (data->max_output && data->total_output > data->max_output) {
173+
php_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output");
174+
return PSFS_ERR_FATAL;
175+
}
163176
bucket = php_stream_bucket_new(stream, estrndup(data->outbuf, bucketlen), bucketlen, 1, 0);
164177
php_stream_bucket_append(buckets_out, bucket);
165178
data->strm.avail_out = data->outbuf_len;
@@ -344,6 +357,16 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi
344357
tmpzval = NULL;
345358
}
346359

360+
if ((tmpzval = zend_hash_str_find_ind(ht, "max_output", sizeof("max_output")-1))) {
361+
zend_long tmp = zval_get_long(tmpzval);
362+
if (tmp <= 0) {
363+
php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp);
364+
} else {
365+
data->max_output = (size_t)tmp;
366+
}
367+
tmpzval = NULL;
368+
}
369+
347370
tmpzval = zend_hash_str_find_ind(ht, "small", sizeof("small")-1);
348371
} else {
349372
tmpzval = filterparams;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
bzip2.decompress: max_output filter parameter
3+
--EXTENSIONS--
4+
bz2
5+
--FILE--
6+
<?php
7+
$original = str_repeat('abcdefgh', 128); // 1024 bytes
8+
$compressed = bzcompress($original);
9+
10+
echo "--- unbounded (no max_output) ---\n";
11+
$fp = fopen('php://temp', 'w+');
12+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE);
13+
fwrite($fp, $compressed);
14+
rewind($fp);
15+
var_dump(strlen(stream_get_contents($fp)));
16+
fclose($fp);
17+
18+
echo "--- max_output above actual size ---\n";
19+
$fp = fopen('php://temp', 'w+');
20+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => 2048]);
21+
fwrite($fp, $compressed);
22+
rewind($fp);
23+
var_dump(strlen(stream_get_contents($fp)));
24+
fclose($fp);
25+
26+
echo "--- max_output below actual size ---\n";
27+
$fp = fopen('php://temp', 'w+');
28+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => 100]);
29+
fwrite($fp, $compressed);
30+
rewind($fp);
31+
var_dump(strlen(stream_get_contents($fp)) <= 100);
32+
fclose($fp);
33+
34+
echo "--- max_output = 0 (invalid) ---\n";
35+
$fp = fopen('php://temp', 'w+');
36+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => 0]);
37+
fclose($fp);
38+
39+
echo "--- max_output = -1 (invalid) ---\n";
40+
$fp = fopen('php://temp', 'w+');
41+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => -1]);
42+
fclose($fp);
43+
?>
44+
--EXPECTF--
45+
--- unbounded (no max_output) ---
46+
int(1024)
47+
--- max_output above actual size ---
48+
int(1024)
49+
--- max_output below actual size ---
50+
51+
Notice: fwrite(): bzip2.decompress: decompressed output exceeded max_output in %s on line %d
52+
bool(true)
53+
--- max_output = 0 (invalid) ---
54+
55+
Warning: stream_filter_append(): Invalid parameter given for max_output (0) in %s on line %d
56+
--- max_output = -1 (invalid) ---
57+
58+
Warning: stream_filter_append(): Invalid parameter given for max_output (-1) in %s on line %d

ext/phar/tests/016.phpt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@ var_dump(file_get_contents($pname . '/d'));
2828
--CLEAN--
2929
<?php unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.phar.php'); ?>
3030
--EXPECTF--
31+
Notice: file_get_contents(): zlib.inflate: decompressed output exceeded max_output in %s on line %d
32+
3133
Warning: file_get_contents(phar://%s/a): Failed to open stream: phar error: internal corruption of phar "%s" (actual filesize mismatch on file "a") in %s on line %d
3234
bool(false)
3335

36+
Notice: file_get_contents(): zlib.inflate: decompressed output exceeded max_output in %s on line %d
37+
3438
Warning: file_get_contents(phar://%s/b): Failed to open stream: phar error: internal corruption of phar "%s" (actual filesize mismatch on file "b") in %s on line %d
3539
bool(false)
3640
string(1) "*"
3741

42+
Notice: file_get_contents(): zlib.inflate: decompressed output exceeded max_output in %s on line %d
43+
3844
Warning: file_get_contents(phar://%s/d): Failed to open stream: phar error: internal corruption of phar "%s" (actual filesize mismatch on file "d") in %s on line %d
3945
bool(false)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
--TEST--
2+
phar decompression: compressed entry round-trips correctly
3+
--EXTENSIONS--
4+
phar
5+
zlib
6+
--INI--
7+
phar.readonly=0
8+
--FILE--
9+
<?php
10+
$fname = __DIR__ . '/' . basename(__FILE__, '.php') . '.phar';
11+
$pname = 'phar://' . $fname;
12+
13+
$phar = new Phar($fname);
14+
$phar['entry.txt'] = 'hello world';
15+
$phar['entry.txt']->compress(Phar::GZ);
16+
17+
echo file_get_contents($pname . '/entry.txt') . "\n";
18+
echo "no crash";
19+
?>
20+
--CLEAN--
21+
<?php
22+
@unlink(__DIR__ . '/' . basename(__FILE__, '.clean.php') . '.phar');
23+
?>
24+
--EXPECT--
25+
hello world
26+
no crash

ext/phar/util.c

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -908,7 +908,15 @@ zend_result phar_open_entry_fp(phar_entry_info *entry, char **error, int follow_
908908
ufp = phar_get_entrypufp(entry);
909909

910910
if ((filtername = phar_decompress_filter(entry, 0)) != NULL) {
911-
filter = php_stream_filter_create(filtername, NULL, 0);
911+
if (entry->uncompressed_filesize) {
912+
zval filterparams;
913+
array_init(&filterparams);
914+
add_assoc_long(&filterparams, "max_output", (zend_long) entry->uncompressed_filesize);
915+
filter = php_stream_filter_create(filtername, &filterparams, 0);
916+
zval_ptr_dtor(&filterparams);
917+
} else {
918+
filter = php_stream_filter_create(filtername, NULL, 0);
919+
}
912920
} else {
913921
filter = NULL;
914922
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
--TEST--
2+
zlib.inflate: max_output filter parameter
3+
--EXTENSIONS--
4+
zlib
5+
--FILE--
6+
<?php
7+
$original = str_repeat('abcdefgh', 128); // 1024 bytes
8+
$compressed = gzdeflate($original);
9+
10+
echo "--- unbounded (no max_output) ---\n";
11+
$fp = fopen('php://temp', 'w+');
12+
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE);
13+
fwrite($fp, $compressed);
14+
rewind($fp);
15+
var_dump(strlen(stream_get_contents($fp)));
16+
fclose($fp);
17+
18+
echo "--- max_output above actual size ---\n";
19+
$fp = fopen('php://temp', 'w+');
20+
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => 2048]);
21+
fwrite($fp, $compressed);
22+
rewind($fp);
23+
var_dump(strlen(stream_get_contents($fp)));
24+
fclose($fp);
25+
26+
echo "--- max_output below actual size ---\n";
27+
$fp = fopen('php://temp', 'w+');
28+
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => 100]);
29+
fwrite($fp, $compressed);
30+
rewind($fp);
31+
var_dump(strlen(stream_get_contents($fp)) <= 100);
32+
fclose($fp);
33+
34+
echo "--- max_output = 0 (invalid) ---\n";
35+
$fp = fopen('php://temp', 'w+');
36+
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => 0]);
37+
fclose($fp);
38+
39+
echo "--- max_output = -1 (invalid) ---\n";
40+
$fp = fopen('php://temp', 'w+');
41+
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => -1]);
42+
fclose($fp);
43+
?>
44+
--EXPECTF--
45+
--- unbounded (no max_output) ---
46+
int(1024)
47+
--- max_output above actual size ---
48+
int(1024)
49+
--- max_output below actual size ---
50+
51+
Notice: fwrite(): zlib.inflate: decompressed output exceeded max_output in %s on line %d
52+
bool(true)
53+
--- max_output = 0 (invalid) ---
54+
55+
Warning: stream_filter_append(): Invalid parameter given for max_output (0) in %s on line %d
56+
--- max_output = -1 (invalid) ---
57+
58+
Warning: stream_filter_append(): Invalid parameter given for max_output (-1) in %s on line %d

ext/zlib/zlib_filter.c

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ typedef struct _php_zlib_filter_data {
2626
size_t inbuf_len;
2727
unsigned char *outbuf;
2828
size_t outbuf_len;
29+
size_t max_output;
30+
size_t total_output;
2931
int persistent;
3032
bool finished; /* for zlib.deflate: signals that no flush is pending */
3133
} php_zlib_filter_data;
@@ -105,6 +107,12 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
105107
if (data->strm.avail_out < data->outbuf_len) {
106108
php_stream_bucket *out_bucket;
107109
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
110+
data->total_output += bucketlen;
111+
if (data->max_output && data->total_output > data->max_output) {
112+
php_error_docref(NULL, E_NOTICE, "zlib.inflate: decompressed output exceeded max_output");
113+
php_stream_bucket_delref(bucket);
114+
return PSFS_ERR_FATAL;
115+
}
108116
out_bucket = php_stream_bucket_new(
109117
stream, estrndup((char *) data->outbuf, bucketlen), bucketlen, 1, 0);
110118
php_stream_bucket_append(buckets_out, out_bucket);
@@ -126,6 +134,11 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
126134
if (data->strm.avail_out < data->outbuf_len) {
127135
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
128136

137+
data->total_output += bucketlen;
138+
if (data->max_output && data->total_output > data->max_output) {
139+
php_error_docref(NULL, E_NOTICE, "zlib.inflate: decompressed output exceeded max_output");
140+
return PSFS_ERR_FATAL;
141+
}
129142
bucket = php_stream_bucket_new(
130143
stream, estrndup((char *) data->outbuf, bucketlen), bucketlen, 1, 0);
131144
php_stream_bucket_append(buckets_out, bucket);
@@ -319,11 +332,11 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
319332
if (strcasecmp(filtername, "zlib.inflate") == 0) {
320333
int windowBits = -MAX_WBITS;
321334

322-
if (filterparams) {
335+
if (filterparams && (Z_TYPE_P(filterparams) == IS_ARRAY || Z_TYPE_P(filterparams) == IS_OBJECT)) {
336+
HashTable *ht = HASH_OF(filterparams);
323337
zval *tmpzval;
324338

325-
if ((Z_TYPE_P(filterparams) == IS_ARRAY || Z_TYPE_P(filterparams) == IS_OBJECT) &&
326-
(tmpzval = zend_hash_str_find_ind(HASH_OF(filterparams), "window", sizeof("window") - 1))) {
339+
if ((tmpzval = zend_hash_str_find_ind(ht, "window", sizeof("window") - 1))) {
327340
/* log-2 base of history window (9 - 15) */
328341
zend_long tmp = zval_get_long(tmpzval);
329342
if (tmp < -MAX_WBITS || tmp > MAX_WBITS + 32) {
@@ -332,6 +345,15 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
332345
windowBits = tmp;
333346
}
334347
}
348+
349+
if ((tmpzval = zend_hash_str_find_ind(ht, "max_output", sizeof("max_output") - 1))) {
350+
zend_long tmp = zval_get_long(tmpzval);
351+
if (tmp <= 0) {
352+
php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp);
353+
} else {
354+
data->max_output = (size_t)tmp;
355+
}
356+
}
335357
}
336358

337359
/* RFC 1951 Inflate */

0 commit comments

Comments
 (0)