diff --git a/ext/phar/phar.c b/ext/phar/phar.c index bee5fe796ebd..fdb37e5e68b2 100644 --- a/ext/phar/phar.c +++ b/ext/phar/phar.c @@ -2786,7 +2786,15 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_flush_ex(phar_archive_data *phar, zen php_stream_flush(entry->cfp); php_stream_filter_remove(filter, 1); php_stream_seek(entry->cfp, 0, SEEK_END); - entry->compressed_filesize = ((uint32_t) php_stream_tell(entry->cfp)) - entry->header_offset; + if ((uint64_t) (php_stream_tell(entry->cfp) - entry->header_offset) > UINT32_MAX) { + if (must_close_old_file) { + php_stream_close(oldfile); + } + php_stream_close(newfile); + spprintf(error, 0, "unable to compress file \"%s\" to new phar \"%s\", compressed file is too large", ZSTR_VAL(entry->filename), ZSTR_VAL(phar->fname)); + goto cleanup; + } + entry->compressed_filesize = php_stream_tell(entry->cfp) - entry->header_offset; /* generate crc on compressed file */ entry->old_flags = entry->flags; entry->is_modified = 1; diff --git a/ext/phar/tar.c b/ext/phar/tar.c index e2d6165b9898..ed05a20c2370 100644 --- a/ext/phar/tar.c +++ b/ext/phar/tar.c @@ -844,9 +844,11 @@ static int phar_tar_writeheaders_int(phar_entry_info *entry, void *argument) /* phar_tar_octal(header.mode, entry->flags & PHAR_ENT_PERM_MASK, sizeof(header.mode)-1); - if (FAILURE == phar_tar_octal(header.size, entry->uncompressed_filesize, sizeof(header.size)-1)) { + /* the reader rejects sizes above this limit, as they overflow the 512 byte padding */ + if (entry->uncompressed_filesize > UINT32_MAX - 511 + || FAILURE == phar_tar_octal(header.size, entry->uncompressed_filesize, sizeof(header.size)-1)) { if (fp->error) { - spprintf(fp->error, 4096, "tar-based phar \"%s\" cannot be created, filename \"%s\" is too large for tar file format", ZSTR_VAL(entry->phar->fname), ZSTR_VAL(entry->filename)); + spprintf(fp->error, 4096, "tar-based phar \"%s\" cannot be created, file \"%s\" is too large for tar file format", ZSTR_VAL(entry->phar->fname), ZSTR_VAL(entry->filename)); } return ZEND_HASH_APPLY_STOP; } diff --git a/ext/phar/tests/gh25053_compressed_size.phpt b/ext/phar/tests/gh25053_compressed_size.phpt new file mode 100644 index 000000000000..fd1fff27a0e9 --- /dev/null +++ b/ext/phar/tests/gh25053_compressed_size.phpt @@ -0,0 +1,70 @@ +--TEST-- +GH-25053: Phar rejects compressed entries whose size exceeds the 32-bit size field +--EXTENSIONS-- +phar +zlib +hash +--SKIPIF-- + +--ENV-- +TEST_TIMEOUT=600 +--INI-- +phar.readonly=0 +--FILE-- + 0) { + $length = min($remaining, strlen($block)); + if (fwrite($fp, substr($block, 0, $length)) !== $length) { + throw new RuntimeException('Could not create the input file'); + } + $remaining -= $length; +} +fclose($fp); +unset($block); + +foreach (['phar', 'zip'] as $format) { + echo $format, "\n"; + $path = __DIR__ . '/gh25053_compressed_size.' . $format; + $phar = $format === 'phar' ? new Phar($path) : new PharData($path); + $phar->startBuffering(); + $phar->addFile($src, 'data'); + + try { + $phar['data']->compress(Phar::GZ); + $phar->stopBuffering(); + echo "Compression unexpectedly succeeded\n"; + } catch (PharException $e) { + // The format limit must be reported, rather than silently narrowing + // the compressed size and writing only that many bytes. + echo $e::class, "\n"; + } +} +?> +--CLEAN-- + +--EXPECT-- +phar +PharException +zip +PharException diff --git a/ext/phar/tests/gh25053_format_limits.phpt b/ext/phar/tests/gh25053_format_limits.phpt new file mode 100644 index 000000000000..c999cc06af8d --- /dev/null +++ b/ext/phar/tests/gh25053_format_limits.phpt @@ -0,0 +1,66 @@ +--TEST-- +GH-25053: Phar rejects mounts, tar entries and zip archives beyond the 32-bit format limits +--EXTENSIONS-- +phar +--SKIPIF-- + +--ENV-- +TEST_TIMEOUT=600 +--INI-- +phar.readonly=0 +--FILE-- +addFromString('small', 'x'); +try { + Phar::mount('phar://' . __DIR__ . '/gh25053_format_limits.phar/big', $src); +} catch (PharException $e) { + echo "mount failed\n"; +} + +$tar = new PharData(__DIR__ . '/gh25053_format_limits.tar'); +$fp = fopen($src, 'rb'); +fseek($fp, 102); +try { + $tar['big'] = $fp; +} catch (PharException $e) { + echo $e->getMessage(), "\n"; +} + +$zip = new PharData(__DIR__ . '/gh25053_format_limits.zip'); +$zip->addFromString('keep', 'old'); +$fp = fopen($src, 'rb'); +fseek($fp, 12); +try { + $zip['big'] = $fp; +} catch (PharException $e) { + echo $e->getMessage(), "\n"; +} +var_dump($zip['keep']->getContent()); +$zip->addFromString('big', 'small'); +var_dump($zip['keep']->getContent()); +var_dump($zip['big']->getContent()); +?> +--CLEAN-- + +--EXPECTF-- +mount failed +tar-based phar "%sgh25053_format_limits.tar" cannot be created, file "big" is too large for tar file format +phar zip flush of "%sgh25053_format_limits.zip" failed: archive is too large for zip file format +string(3) "old" +string(3) "old" +string(5) "small" diff --git a/ext/phar/util.c b/ext/phar/util.c index 2d636e9b84f5..f27706bb49d5 100644 --- a/ext/phar/util.c +++ b/ext/phar/util.c @@ -275,6 +275,11 @@ zend_result phar_mount_entry(phar_archive_data *phar, const char *filename, size return FAILURE; } } else { + if ((uint64_t) ssb.sb.st_size > UINT32_MAX) { + efree(entry.tmp); + zend_string_efree(entry.filename); + return FAILURE; + } entry.is_dir = false; entry.uncompressed_filesize = entry.compressed_filesize = ssb.sb.st_size; } diff --git a/ext/phar/zip.c b/ext/phar/zip.c index 13c34d8a3b0f..e3283532b3cd 100644 --- a/ext/phar/zip.c +++ b/ext/phar/zip.c @@ -1025,7 +1025,11 @@ static int phar_zip_changed_apply_int(phar_entry_info *entry, void *arg) /* {{{ php_stream_flush(entry->cfp); php_stream_filter_remove(filter, 1); php_stream_seek(entry->cfp, 0, SEEK_END); - entry->compressed_filesize = (uint32_t) php_stream_tell(entry->cfp); + if ((uint64_t) php_stream_tell(entry->cfp) > UINT32_MAX) { + spprintf(p->error, 0, "unable to compress file \"%s\" to zip-based phar \"%s\", compressed file is too large", ZSTR_VAL(entry->filename), ZSTR_VAL(entry->phar->fname)); + return ZEND_HASH_APPLY_STOP; + } + entry->compressed_filesize = php_stream_tell(entry->cfp); PHAR_SET_32(central.compsize, entry->compressed_filesize); PHAR_SET_32(local.compsize, entry->compressed_filesize); /* generate crc on compressed file */ @@ -1441,6 +1445,21 @@ ZEND_ATTRIBUTE_NONNULL_ARGS(1, 4) int phar_zip_flush(phar_archive_data *phar, ze } /* save zip */ + /* zip64 is not supported, so the central directory offset and size must fit in 32 bits */ + if ((uint64_t) php_stream_tell(pass.filefp) > UINT32_MAX + || (uint64_t) php_stream_tell(pass.centralfp) > UINT32_MAX) { + spprintf(error, 4096, "phar zip flush of \"%s\" failed: archive is too large for zip file format", ZSTR_VAL(phar->fname)); + php_stream_close(pass.centralfp); + if (phar->fp && pass.free_fp) { + php_stream_close(phar->fp); + } else if (must_close_old_file) { + php_stream_close(oldfile); + } + phar->fp = pass.filefp; + phar->is_brandnew = 0; + return EOF; + } + cdir_size = php_stream_tell(pass.centralfp); cdir_offset = php_stream_tell(pass.filefp); PHAR_SET_32(eocd.cdir_size, cdir_size);