Skip to content

Commit 248d688

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 248d688

4 files changed

Lines changed: 164 additions & 3 deletions

File tree

ext/bz2/bz2_filter.c

Lines changed: 23 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,16 @@ 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+
zend_long tmp = zval_get_long(tmpzval);
431+
if (tmp <= 0) {
432+
php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp);
433+
} else {
434+
data->max_output = (size_t)tmp;
435+
}
436+
tmpzval = NULL;
437+
}
438+
416439
tmpzval = zend_hash_str_find_ind(ht, "small", sizeof("small")-1);
417440
} else {
418441
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
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
@@ -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,15 @@ 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+
zend_long tmp = zval_get_long(tmpzval);
426+
if (tmp <= 0) {
427+
php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp);
428+
} else {
429+
data->max_output = (size_t)tmp;
430+
}
431+
}
410432
}
411433

412434
/* Save configuration for reset */

0 commit comments

Comments
 (0)