1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2026-01-13 05:42:30 +03:00

ZIP Exports: Added limit to ZIP file size before extraction

Checks files within the ZIP again the app upload file limit
before using/streaming/extracting, to help ensure that they do no exceed
what might be expected on that instance, and to prevent disk exhaustion
via things like super high compression ratio files.

Thanks to Jeong Woo Lee (eclipse07077-ljw) for reporting.
This commit is contained in:
Dan Brown
2025-12-29 23:08:18 +00:00
parent 38d3697246
commit 88d86df66f
6 changed files with 89 additions and 1 deletions

View File

@@ -58,6 +58,16 @@ class ZipExportReader
{
$this->open();
$info = $this->zip->statName('data.json');
if ($info === false) {
throw new ZipExportException(trans('errors.import_zip_cant_decode_data'));
}
$maxSize = max(intval(config()->get('app.upload_limit')), 1) * 1000000;
if ($info['size'] > $maxSize) {
throw new ZipExportException(trans('errors.import_zip_data_too_large'));
}
// Validate json data exists, including metadata
$jsonData = $this->zip->getFromName('data.json') ?: '';
$importData = json_decode($jsonData, true);
@@ -73,6 +83,17 @@ class ZipExportReader
return $this->zip->statName("files/{$fileName}") !== false;
}
public function fileWithinSizeLimit(string $fileName): bool
{
$fileInfo = $this->zip->statName("files/{$fileName}");
if ($fileInfo === false) {
return false;
}
$maxSize = max(intval(config()->get('app.upload_limit')), 1) * 1000000;
return $fileInfo['size'] <= $maxSize;
}
/**
* @return false|resource
*/

View File

@@ -13,7 +13,6 @@ class ZipFileReferenceRule implements ValidationRule
) {
}
/**
* @inheritDoc
*/
@@ -23,6 +22,13 @@ class ZipFileReferenceRule implements ValidationRule
$fail('validation.zip_file')->translate();
}
if (!$this->context->zipReader->fileWithinSizeLimit($value)) {
$fail('validation.zip_file_size')->translate([
'attribute' => $value,
'size' => config('app.upload_limit'),
]);
}
if (!empty($this->acceptedMimes)) {
$fileMime = $this->context->zipReader->sniffFileMime($value);
if (!in_array($fileMime, $this->acceptedMimes)) {

View File

@@ -265,6 +265,12 @@ class ZipImportRunner
protected function zipFileToUploadedFile(string $fileName, ZipExportReader $reader): UploadedFile
{
if (!$reader->fileWithinSizeLimit($fileName)) {
throw new ZipImportException([
"File $fileName exceeds app upload limit."
]);
}
$tempPath = tempnam(sys_get_temp_dir(), 'bszipextract');
$fileStream = $reader->streamFile($fileName);
$tempStream = fopen($tempPath, 'wb');

View File

@@ -109,6 +109,7 @@ return [
'import_zip_cant_read' => 'Could not read ZIP file.',
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
'import_zip_data_too_large' => 'ZIP data.json content exceeds the configured application maximum upload size.',
'import_validation_failed' => 'Import ZIP failed to validate with errors:',
'import_zip_failed_notification' => 'Failed to import ZIP file.',
'import_perms_books' => 'You are lacking the required permissions to create books.',

View File

@@ -106,6 +106,7 @@ return [
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
'zip_file_size' => 'The file :attribute must not exceed :size MB.',
'zip_file_mime' => 'The :attribute needs to reference a file of type :validTypes, found :foundType.',
'zip_model_expected' => 'Data object expected but ":type" found.',
'zip_unique' => 'The :attribute must be unique for the object type within the ZIP.',

View File

@@ -5,6 +5,7 @@ namespace Tests\Exports;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\ZipImportException;
use BookStack\Exports\ZipExports\ZipImportRunner;
use BookStack\Uploads\Image;
use Tests\TestCase;
@@ -431,4 +432,56 @@ class ZipImportRunnerTest extends TestCase
ZipTestHelper::deleteZipForImport($import);
}
public function test_error_thrown_if_zip_item_exceeds_app_file_upload_limit()
{
$tempFile = tempnam(sys_get_temp_dir(), 'bs-zip-test');
file_put_contents($tempFile, str_repeat('a', 2500000));
$parent = $this->entities->chapter();
config()->set('app.upload_limit', 1);
$import = ZipTestHelper::importFromData([], [
'page' => [
'name' => 'Page A',
'html' => '<p>Hello</p>',
'attachments' => [
[
'name' => 'Text attachment',
'file' => 'file_attachment'
]
],
],
], [
'file_attachment' => $tempFile,
]);
$this->asAdmin();
$this->expectException(ZipImportException::class);
$this->expectExceptionMessage('The file file_attachment must not exceed 1 MB.');
$this->runner->run($import, $parent);
ZipTestHelper::deleteZipForImport($import);
}
public function test_error_thrown_if_zip_data_exceeds_app_file_upload_limit()
{
$parent = $this->entities->chapter();
config()->set('app.upload_limit', 1);
$import = ZipTestHelper::importFromData([], [
'page' => [
'name' => 'Page A',
'html' => '<p>' . str_repeat('a', 2500000) . '</p>',
],
]);
$this->asAdmin();
$this->expectException(ZipImportException::class);
$this->expectExceptionMessage('ZIP data.json content exceeds the configured application maximum upload size.');
$this->runner->run($import, $parent);
ZipTestHelper::deleteZipForImport($import);
}
}