diff --git a/ext/bz2/bz2_filter.c b/ext/bz2/bz2_filter.c index 0fb982c0353c..597d5931f672 100644 --- a/ext/bz2/bz2_filter.c +++ b/ext/bz2/bz2_filter.c @@ -35,6 +35,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; enum strm_status status; /* Decompress option */ unsigned int small_footprint : 1; /* Decompress option */ @@ -137,6 +139,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_error_docref(NULL, E_NOTICE, "bzip2.decompress: decompressed output exceeded max_output"); + php_stream_bucket_delref(bucket); + 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; @@ -160,6 +168,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; @@ -344,6 +357,16 @@ static php_stream_filter *php_bz2_filter_create(const char *filtername, zval *fi tmpzval = NULL; } + if ((tmpzval = zend_hash_str_find_ind(ht, "max_output", sizeof("max_output")-1))) { + zend_long tmp = zval_get_long(tmpzval); + if (tmp <= 0) { + php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp); + } else { + data->max_output = (size_t)tmp; + } + tmpzval = NULL; + } + tmpzval = zend_hash_str_find_ind(ht, "small", sizeof("small")-1); } else { tmpzval = filterparams; 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..da3ada9318d4 --- /dev/null +++ b/ext/bz2/tests/bz2_filter_decompress_max_output.phpt @@ -0,0 +1,58 @@ +--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 = 0 (invalid) ---\n"; +$fp = fopen('php://temp', 'w+'); +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+'); +stream_filter_append($fp, 'bzip2.decompress', STREAM_FILTER_WRITE, ['max_output' => -1]); +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 = 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 diff --git a/ext/phar/tests/016.phpt b/ext/phar/tests/016.phpt index d14c1292a6e2..2d0eb485320e 100644 --- a/ext/phar/tests/016.phpt +++ b/ext/phar/tests/016.phpt @@ -28,12 +28,18 @@ var_dump(file_get_contents($pname . '/d')); --CLEAN-- --EXPECTF-- +Notice: file_get_contents(): zlib.inflate: decompressed output exceeded max_output in %s on line %d + 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 bool(false) +Notice: file_get_contents(): zlib.inflate: decompressed output exceeded max_output in %s on line %d + 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 bool(false) string(1) "*" +Notice: file_get_contents(): zlib.inflate: decompressed output exceeded max_output in %s on line %d + 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 bool(false) diff --git a/ext/phar/tests/gh-ss012-decomp-bounds.phpt b/ext/phar/tests/gh-ss012-decomp-bounds.phpt new file mode 100644 index 000000000000..dfcbc8985fa3 --- /dev/null +++ b/ext/phar/tests/gh-ss012-decomp-bounds.phpt @@ -0,0 +1,26 @@ +--TEST-- +phar decompression: compressed entry round-trips correctly +--EXTENSIONS-- +phar +zlib +--INI-- +phar.readonly=0 +--FILE-- +compress(Phar::GZ); + +echo file_get_contents($pname . '/entry.txt') . "\n"; +echo "no crash"; +?> +--CLEAN-- + +--EXPECT-- +hello world +no crash diff --git a/ext/phar/util.c b/ext/phar/util.c index a1f9863ae3a9..64b3c259c940 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -908,7 +908,15 @@ zend_result phar_open_entry_fp(phar_entry_info *entry, char **error, int follow_ ufp = phar_get_entrypufp(entry); if ((filtername = phar_decompress_filter(entry, 0)) != NULL) { - filter = php_stream_filter_create(filtername, NULL, 0); + if (entry->uncompressed_filesize) { + zval filterparams; + array_init(&filterparams); + add_assoc_long(&filterparams, "max_output", (zend_long) entry->uncompressed_filesize); + filter = php_stream_filter_create(filtername, &filterparams, 0); + zval_ptr_dtor(&filterparams); + } else { + filter = php_stream_filter_create(filtername, NULL, 0); + } } else { filter = NULL; } 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..313be502420f --- /dev/null +++ b/ext/zlib/tests/zlib_filter_inflate_max_output.phpt @@ -0,0 +1,58 @@ +--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 = 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); +?> +--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 = 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 diff --git a/ext/zlib/zlib_filter.c b/ext/zlib/zlib_filter.c index e5491afec391..098b022bd129 100644 --- a/ext/zlib/zlib_filter.c +++ b/ext/zlib/zlib_filter.c @@ -26,6 +26,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 persistent; bool finished; /* for zlib.deflate: signals that no flush is pending */ } php_zlib_filter_data; @@ -105,6 +107,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_error_docref(NULL, E_NOTICE, "zlib.inflate: decompressed output exceeded max_output"); + php_stream_bucket_delref(bucket); + 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); @@ -126,6 +134,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); @@ -319,11 +332,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) { @@ -332,6 +345,15 @@ 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))) { + zend_long tmp = zval_get_long(tmpzval); + if (tmp <= 0) { + php_error_docref(NULL, E_WARNING, "Invalid parameter given for max_output (" ZEND_LONG_FMT ")", tmp); + } else { + data->max_output = (size_t)tmp; + } + } } /* RFC 1951 Inflate */