diff --git a/ext/bz2/bz2_filter.c b/ext/bz2/bz2_filter.c index f1262c1ea99e..cd47c10fa162 100644 --- a/ext/bz2/bz2_filter.c +++ b/ext/bz2/bz2_filter.c @@ -33,6 +33,8 @@ typedef struct _php_bz2_filter_data { char *outbuf; size_t inbuf_len; size_t outbuf_len; + size_t max_output; + size_t total_output; bool persistent; bool expect_concatenated : 1; /* Decompress option */ @@ -142,6 +144,12 @@ static php_stream_filter_status_t php_bz2_decompress_filter( if (data->strm.avail_out < data->outbuf_len) { php_stream_bucket *out_bucket; size_t bucketlen = data->outbuf_len - data->strm.avail_out; + data->total_output += bucketlen; + if (data->max_output && data->total_output > data->max_output) { + php_stream_bucket_delref(bucket); + php_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output"); + return PSFS_ERR_FATAL; + } out_bucket = php_stream_bucket_new(stream, estrndup(data->outbuf, bucketlen), bucketlen, 1, 0); php_stream_bucket_append(buckets_out, out_bucket); data->strm.avail_out = data->outbuf_len; @@ -165,6 +173,11 @@ static php_stream_filter_status_t php_bz2_decompress_filter( if (data->strm.avail_out < data->outbuf_len) { size_t bucketlen = data->outbuf_len - data->strm.avail_out; + data->total_output += bucketlen; + if (data->max_output && data->total_output > data->max_output) { + php_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output"); + return PSFS_ERR_FATAL; + } bucket = php_stream_bucket_new(stream, estrndup(data->outbuf, bucketlen), bucketlen, 1, 0); php_stream_bucket_append(buckets_out, bucket); data->strm.avail_out = data->outbuf_len; @@ -207,6 +220,7 @@ static zend_result php_bz2_decompress_seek( data->strm.next_out = data->outbuf; data->strm.avail_out = data->outbuf_len; data->status = PHP_BZ2_UNINITIALIZED; + data->total_output = 0; /* Note: We don't reinitialize here - it will be done on first use in the filter function */ @@ -398,6 +412,7 @@ static php_stream_filter *php_bz2_decompress_filter_create(zval *filter_params, php_stream_filter_seekable_t write_seekable = PSFS_SEEKABLE_ALWAYS; bool small_footprint = false; bool expect_concatenated = false; + size_t max_output = 0; if (filter_params) { if (UNEXPECTED( @@ -440,6 +455,20 @@ static php_stream_filter *php_bz2_decompress_filter_create(zval *filter_params, if (small) { small_footprint = zend_is_true(small); } + + const zval *max_output_zv = zend_hash_str_find_ind(filter_params_ht, ZEND_STRL("max_output")); + if (max_output_zv) { + bool failed = false; + zend_long max_output_long = zval_try_get_long(max_output_zv, &failed); + if (UNEXPECTED(failed)) { + php_error_docref("filters.compression", E_WARNING, "Max output parameter must be of type int, %s given", zend_zval_type_name(max_output_zv)); + return NULL; + } else if (max_output_long <= 0) { + php_error_docref("filters.compression", E_WARNING, "Max output must be greater than 0, " ZEND_LONG_FMT " given", max_output_long); + return NULL; + } + max_output = (size_t) max_output_long; + } } } @@ -447,6 +476,7 @@ static php_stream_filter *php_bz2_decompress_filter_create(zval *filter_params, /* Save configuration for reset */ data->small_footprint = small_footprint; data->expect_concatenated = expect_concatenated; + data->max_output = max_output; data->status = PHP_BZ2_UNINITIALIZED; return php_stream_filter_alloc(&php_bz2_decompress_ops, data, persistent, PSFS_SEEKABLE_START, write_seekable); diff --git a/ext/bz2/tests/bz2_filter_decompress_max_output.phpt b/ext/bz2/tests/bz2_filter_decompress_max_output.phpt new file mode 100644 index 000000000000..9b7b011d7529 --- /dev/null +++ b/ext/bz2/tests/bz2_filter_decompress_max_output.phpt @@ -0,0 +1,88 @@ +--TEST-- +bzip2.decompress: max_output filter parameter +--EXTENSIONS-- +bz2 +--FILE-- + 2048]); +fwrite($fp, $compressed); +rewind($fp); +var_dump(strlen(stream_get_contents($fp))); +fclose($fp); + +echo "--- max_output below actual size ---\n"; +$fp = fopen('php://temp', 'w+'); +stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => 100]); +fwrite($fp, $compressed); +rewind($fp); +var_dump(strlen(stream_get_contents($fp)) <= 100); +fclose($fp); + +echo "--- max_output survives rewind ---\n"; +$fp = fopen('php://temp', 'w+'); +fwrite($fp, $compressed); +rewind($fp); +stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_READ, ['max_output' => 1024]); +var_dump(strlen(stream_get_contents($fp))); +rewind($fp); +var_dump(strlen(stream_get_contents($fp))); +fclose($fp); + +echo "--- max_output = 0 (invalid) ---\n"; +$fp = fopen('php://temp', 'w+'); +var_dump(stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => 0])); +fclose($fp); + +echo "--- max_output = -1 (invalid) ---\n"; +$fp = fopen('php://temp', 'w+'); +var_dump(stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => -1])); +fclose($fp); + +echo "--- max_output = array (invalid type) ---\n"; +$fp = fopen('php://temp', 'w+'); +var_dump(stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => []])); +fclose($fp); +?> +--EXPECTF-- +--- unbounded (no max_output) --- +int(1024) +--- max_output above actual size --- +int(1024) +--- max_output below actual size --- + +Notice: fwrite(): bzip2.decompress: decompressed output exceeded max_output in %s on line %d +bool(true) +--- max_output survives rewind --- +int(1024) +int(1024) +--- max_output = 0 (invalid) --- + +Warning: stream_filter_append(): Max output must be greater than 0, 0 given in %s on line %d + +Warning: stream_filter_append(): Unable to create or locate filter "bzip2.decompress" in %s on line %d +bool(false) +--- max_output = -1 (invalid) --- + +Warning: stream_filter_append(): Max output must be greater than 0, -1 given in %s on line %d + +Warning: stream_filter_append(): Unable to create or locate filter "bzip2.decompress" in %s on line %d +bool(false) +--- max_output = array (invalid type) --- + +Warning: stream_filter_append(): Max output parameter must be of type int, array given in %s on line %d + +Warning: stream_filter_append(): Unable to create or locate filter "bzip2.decompress" in %s on line %d +bool(false) diff --git a/ext/zlib/tests/zlib_filter_inflate_max_output.phpt b/ext/zlib/tests/zlib_filter_inflate_max_output.phpt new file mode 100644 index 000000000000..3321eeba59ea --- /dev/null +++ b/ext/zlib/tests/zlib_filter_inflate_max_output.phpt @@ -0,0 +1,79 @@ +--TEST-- +zlib.inflate: max_output filter parameter +--EXTENSIONS-- +zlib +--FILE-- + 2048]); +fwrite($fp, $compressed); +rewind($fp); +var_dump(strlen(stream_get_contents($fp))); +fclose($fp); + +echo "--- max_output below actual size ---\n"; +$fp = fopen('php://temp', 'w+'); +stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => 100]); +fwrite($fp, $compressed); +rewind($fp); +var_dump(strlen(stream_get_contents($fp)) <= 100); +fclose($fp); + +echo "--- max_output survives rewind ---\n"; +$fp = fopen('php://temp', 'w+'); +fwrite($fp, $compressed); +rewind($fp); +stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_READ, ['max_output' => 1024]); +var_dump(strlen(stream_get_contents($fp))); +rewind($fp); +var_dump(strlen(stream_get_contents($fp))); +fclose($fp); + +echo "--- max_output = 0 (invalid) ---\n"; +$fp = fopen('php://temp', 'w+'); +stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => 0]); +fclose($fp); + +echo "--- max_output = -1 (invalid) ---\n"; +$fp = fopen('php://temp', 'w+'); +stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => -1]); +fclose($fp); + +echo "--- max_output = array (invalid type) ---\n"; +$fp = fopen('php://temp', 'w+'); +stream_filter_append($fp, 'zlib.inflate', STREAM_FILTER_WRITE, ['max_output' => []]); +fclose($fp); +?> +--EXPECTF-- +--- unbounded (no max_output) --- +int(1024) +--- max_output above actual size --- +int(1024) +--- max_output below actual size --- + +Notice: fwrite(): zlib.inflate: decompressed output exceeded max_output in %s on line %d +bool(true) +--- max_output survives rewind --- +int(1024) +int(1024) +--- max_output = 0 (invalid) --- + +Warning: stream_filter_append(): Invalid parameter given for max_output (0) in %s on line %d +--- max_output = -1 (invalid) --- + +Warning: stream_filter_append(): Invalid parameter given for max_output (-1) in %s on line %d +--- max_output = array (invalid type) --- + +Warning: stream_filter_append(): Invalid type for max_output, expected int in %s on line %d diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c index e71308db48d6..14fac205a772 100644 --- a/ext/zlib/zlib_filter.c +++ b/ext/zlib/zlib_filter.c @@ -24,6 +24,8 @@ typedef struct _php_zlib_filter_data { size_t inbuf_len; unsigned char *outbuf; size_t outbuf_len; + size_t max_output; + size_t total_output; int windowBits; bool persistent; bool finished; /* for zlib.deflate: signals that no flush is pending */ @@ -109,6 +111,12 @@ static php_stream_filter_status_t php_zlib_inflate_filter( if (data->strm.avail_out < data->outbuf_len) { php_stream_bucket *out_bucket; size_t bucketlen = data->outbuf_len - data->strm.avail_out; + data->total_output += bucketlen; + if (data->max_output && data->total_output > data->max_output) { + php_stream_bucket_delref(bucket); + php_error_docref(NULL, E_NOTICE, "zlib.inflate: decompressed output exceeded max_output"); + return PSFS_ERR_FATAL; + } out_bucket = php_stream_bucket_new( stream, estrndup((char *) data->outbuf, bucketlen), bucketlen, 1, 0); php_stream_bucket_append(buckets_out, out_bucket); @@ -130,6 +138,11 @@ static php_stream_filter_status_t php_zlib_inflate_filter( if (data->strm.avail_out < data->outbuf_len) { size_t bucketlen = data->outbuf_len - data->strm.avail_out; + data->total_output += bucketlen; + if (data->max_output && data->total_output > data->max_output) { + php_error_docref(NULL, E_NOTICE, "zlib.inflate: decompressed output exceeded max_output"); + return PSFS_ERR_FATAL; + } bucket = php_stream_bucket_new( stream, estrndup((char *) data->outbuf, bucketlen), bucketlen, 1, 0); php_stream_bucket_append(buckets_out, bucket); @@ -184,6 +197,7 @@ static zend_result php_zlib_inflate_seek( data->strm.next_out = data->outbuf; data->strm.avail_out = data->outbuf_len; data->finished = false; + data->total_output = 0; return SUCCESS; } @@ -412,11 +426,11 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f if (strcasecmp(filtername, "zlib.inflate") == 0) { int windowBits = -MAX_WBITS; - if (filterparams) { + if (filterparams && (Z_TYPE_P(filterparams) == IS_ARRAY || Z_TYPE_P(filterparams) == IS_OBJECT)) { + HashTable *ht = HASH_OF(filterparams); zval *tmpzval; - if ((Z_TYPE_P(filterparams) == IS_ARRAY || Z_TYPE_P(filterparams) == IS_OBJECT) && - (tmpzval = zend_hash_str_find_ind(HASH_OF(filterparams), "window", sizeof("window") - 1))) { + if ((tmpzval = zend_hash_str_find_ind(ht, "window", sizeof("window") - 1))) { /* log-2 base of history window (9 - 15) */ zend_long tmp = zval_get_long(tmpzval); if (tmp < -MAX_WBITS || tmp > MAX_WBITS + 32) { @@ -425,6 +439,18 @@ static php_stream_filter *php_zlib_filter_create(const char *filtername, zval *f windowBits = tmp; } } + + if ((tmpzval = zend_hash_str_find_ind(ht, "max_output", sizeof("max_output") - 1))) { + bool failed; + zend_long tmp = zval_try_get_long(tmpzval, &failed); + if (failed) { + php_error_docref("filters.compression", E_WARNING, "Invalid type for max_output, expected int"); + } else if (tmp <= 0) { + php_error_docref("filters.compression", E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp); + } else { + data->max_output = (size_t)tmp; + } + } } /* Save configuration for reset */