Skip to content

Commit af57e75

Browse files
committed
zlib/bz2: add max_output filter param to cap decompression output
Optional max_output parameter on zlib.inflate and bzip2.decompress caps bytes emitted by the filter. When the instance has a cap set and exceeds it, the current bucket is dropped and the filter returns PSFS_ERR_FATAL, stopping decompression amplification mid-stream instead of after the full payload lands on the sink. The parameter is opt-in. Omitting it preserves existing behavior for all current callers. Userland opts in via stream_filter_append($stream, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => N]).
1 parent 3aafc64 commit af57e75

4 files changed

Lines changed: 186 additions & 3 deletions

File tree

ext/bz2/bz2_filter.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ typedef struct _php_bz2_filter_data {
3333
char *outbuf;
3434
size_t inbuf_len;
3535
size_t outbuf_len;
36+
size_t max_output;
37+
size_t total_output;
3638

3739
enum strm_status status; /* Decompress option */
3840
unsigned int small_footprint : 1; /* Decompress option */
@@ -139,6 +141,12 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
139141
if (data->strm.avail_out < data->outbuf_len) {
140142
php_stream_bucket *out_bucket;
141143
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
144+
data->total_output += bucketlen;
145+
if (data->max_output && data->total_output > data->max_output) {
146+
php_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output");
147+
php_stream_bucket_delref(bucket);
148+
return PSFS_ERR_FATAL;
149+
}
142150
out_bucket = php_stream_bucket_new(stream, estrndup(data->outbuf, bucketlen), bucketlen, 1, 0);
143151
php_stream_bucket_append(buckets_out, out_bucket);
144152
data->strm.avail_out = data->outbuf_len;
@@ -162,6 +170,11 @@ static php_stream_filter_status_t php_bz2_decompress_filter(
162170
if (data->strm.avail_out < data->outbuf_len) {
163171
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
164172

173+
data->total_output += bucketlen;
174+
if (data->max_output && data->total_output > data->max_output) {
175+
php_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output");
176+
return PSFS_ERR_FATAL;
177+
}
165178
bucket = php_stream_bucket_new(stream, estrndup(data->outbuf, bucketlen), bucketlen, 1, 0);
166179
php_stream_bucket_append(buckets_out, bucket);
167180
data->strm.avail_out = data->outbuf_len;
@@ -413,6 +426,19 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi
413426
tmpzval = NULL;
414427
}
415428

429+
if ((tmpzval = zend_hash_str_find_ind(ht, "max_output", sizeof("max_output")-1))) {
430+
bool failed;
431+
zend_long tmp = zval_try_get_long(tmpzval, &failed);
432+
if (failed) {
433+
php_error_docref(NULL, E_WARNING, "Invalid type for max_output, expected int");
434+
} else if (tmp <= 0) {
435+
php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp);
436+
} else {
437+
data->max_output = (size_t)tmp;
438+
}
439+
tmpzval = NULL;
440+
}
441+
416442
tmpzval = zend_hash_str_find_ind(ht, "small", sizeof("small")-1);
417443
} else {
418444
tmpzval = filterparams;
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
echo "--- max_output = array (invalid type) ---\n";
45+
$fp = fopen('php://temp', 'w+');
46+
stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => []]);
47+
fclose($fp);
48+
?>
49+
--EXPECTF--
50+
--- unbounded (no max_output) ---
51+
int(1024)
52+
--- max_output above actual size ---
53+
int(1024)
54+
--- max_output below actual size ---
55+
56+
Notice: fwrite(): bzip2.decompress: decompressed output exceeded max_output in %s on line %d
57+
bool(true)
58+
--- max_output = 0 (invalid) ---
59+
60+
Warning: stream_filter_append(): Invalid parameter given for max_output (0) in %s on line %d
61+
--- max_output = -1 (invalid) ---
62+
63+
Warning: stream_filter_append(): Invalid parameter given for max_output (-1) in %s on line %d
64+
--- max_output = array (invalid type) ---
65+
66+
Warning: stream_filter_append(): Invalid type for max_output, expected int in %s on line %d
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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+
echo "--- max_output = array (invalid type) ---\n";
45+
$fp = fopen('php://temp', 'w+');
46+
stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => []]);
47+
fclose($fp);
48+
?>
49+
--EXPECTF--
50+
--- unbounded (no max_output) ---
51+
int(1024)
52+
--- max_output above actual size ---
53+
int(1024)
54+
--- max_output below actual size ---
55+
56+
Notice: fwrite(): zlib.inflate: decompressed output exceeded max_output in %s on line %d
57+
bool(true)
58+
--- max_output = 0 (invalid) ---
59+
60+
Warning: stream_filter_append(): Invalid parameter given for max_output (0) in %s on line %d
61+
--- max_output = -1 (invalid) ---
62+
63+
Warning: stream_filter_append(): Invalid parameter given for max_output (-1) in %s on line %d
64+
--- max_output = array (invalid type) ---
65+
66+
Warning: stream_filter_append(): Invalid type for max_output, expected int in %s on line %d

ext/zlib/zlib_filter.c

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ typedef struct _php_zlib_filter_data {
2424
size_t inbuf_len;
2525
unsigned char *outbuf;
2626
size_t outbuf_len;
27+
size_t max_output;
28+
size_t total_output;
2729
int persistent;
2830
bool finished; /* for zlib.deflate: signals that no flush is pending */
2931
int windowBits;
@@ -104,6 +106,12 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
104106
if (data->strm.avail_out < data->outbuf_len) {
105107
php_stream_bucket *out_bucket;
106108
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
109+
data->total_output += bucketlen;
110+
if (data->max_output && data->total_output > data->max_output) {
111+
php_error_docref(NULL, E_NOTICE, "zlib.inflate: decompressed output exceeded max_output");
112+
php_stream_bucket_delref(bucket);
113+
return PSFS_ERR_FATAL;
114+
}
107115
out_bucket = php_stream_bucket_new(
108116
stream, estrndup((char *) data->outbuf, bucketlen), bucketlen, 1, 0);
109117
php_stream_bucket_append(buckets_out, out_bucket);
@@ -125,6 +133,11 @@ static php_stream_filter_status_t php_zlib_inflate_filter(
125133
if (data->strm.avail_out < data->outbuf_len) {
126134
size_t bucketlen = data->outbuf_len - data->strm.avail_out;
127135

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

397-
if (filterparams) {
410+
if (filterparams && (Z_TYPE_P(filterparams) == IS_ARRAY || Z_TYPE_P(filterparams) == IS_OBJECT)) {
411+
HashTable *ht = HASH_OF(filterparams);
398412
zval *tmpzval;
399413

400-
if ((Z_TYPE_P(filterparams) == IS_ARRAY || Z_TYPE_P(filterparams) == IS_OBJECT) &&
401-
(tmpzval = zend_hash_str_find_ind(HASH_OF(filterparams), "window", sizeof("window") - 1))) {
414+
if ((tmpzval = zend_hash_str_find_ind(ht, "window", sizeof("window") - 1))) {
402415
/* log-2 base of history window (9 - 15) */
403416
zend_long tmp = zval_get_long(tmpzval);
404417
if (tmp < -MAX_WBITS || tmp > MAX_WBITS + 32) {
@@ -407,6 +420,18 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f
407420
windowBits = tmp;
408421
}
409422
}
423+
424+
if ((tmpzval = zend_hash_str_find_ind(ht, "max_output", sizeof("max_output") - 1))) {
425+
bool failed;
426+
zend_long tmp = zval_try_get_long(tmpzval, &failed);
427+
if (failed) {
428+
php_error_docref(NULL, E_WARNING, "Invalid type for max_output, expected int");
429+
} else if (tmp <= 0) {
430+
php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp);
431+
} else {
432+
data->max_output = (size_t)tmp;
433+
}
434+
}
410435
}
411436

412437
/* Save configuration for reset */

0 commit comments

Comments
 (0)