From 473cd4f7efaef2a530329898e79e5cbfe2820468 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 7 Apr 2025 08:27:38 +0200 Subject: [PATCH] Check max ZIP uncompressed size --- packages/tests/fixtures/zip-bomb.zip | Bin 0 -> 42374 bytes .../tests/src/api/check-params/user-import.ts | 3 ++- server/core/helpers/unzip.ts | 25 +++++++++++++++++- .../lib/user-import-export/user-importer.ts | 12 +++++++-- 4 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 packages/tests/fixtures/zip-bomb.zip diff --git a/packages/tests/fixtures/zip-bomb.zip b/packages/tests/fixtures/zip-bomb.zip new file mode 100644 index 0000000000000000000000000000000000000000..b4d00682f287342d9afab1d70c03075d9a48eed1 GIT binary patch literal 42374 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 DBG6lk literal 0 HcmV?d00001 diff --git a/packages/tests/src/api/check-params/user-import.ts b/packages/tests/src/api/check-params/user-import.ts index 4abd9e0cf..545a3e46f 100644 --- a/packages/tests/src/api/check-params/user-import.ts +++ b/packages/tests/src/api/check-params/user-import.ts @@ -122,7 +122,8 @@ describe('Test user import API validators', function () { 'export-without-videos.zip', 'export-bad-structure.zip', 'export-bad-structure.zip', - 'export-crash.zip' + 'export-crash.zip', + 'zip-bomb.zip' ] const tokens: string[] = [] diff --git a/server/core/helpers/unzip.ts b/server/core/helpers/unzip.ts index 00defd69a..5e04d9aaf 100644 --- a/server/core/helpers/unzip.ts +++ b/server/core/helpers/unzip.ts @@ -7,7 +7,14 @@ import { logger, loggerTagsFactory } from './logger.js' const lTags = loggerTagsFactory('unzip') -export async function unzip (source: string, destination: string) { +export async function unzip (options: { + source: string + destination: string + maxSize: number // in bytes + maxFiles: number +}) { + const { source, destination } = options + await ensureDir(destination) logger.info(`Unzip ${source} to ${destination}`, lTags()) @@ -18,9 +25,25 @@ export async function unzip (source: string, destination: string) { zipFile.on('error', err => rej(err)) + let decompressedSize = 0 + let entries = 0 + zipFile.readEntry() zipFile.on('entry', async entry => { + decompressedSize += entry.uncompressedSize + entries++ + + if (decompressedSize > options.maxSize) { + zipFile.close() + return rej(new Error(`Unzipped size exceeds ${options.maxSize} bytes`)) + } + + if (entries > options.maxFiles) { + zipFile.close() + return rej(new Error(`Unzipped files count exceeds ${options.maxFiles}`)) + } + const entryPath = join(destination, entry.fileName) try { diff --git a/server/core/lib/user-import-export/user-importer.ts b/server/core/lib/user-import-export/user-importer.ts index f48344c56..53f0a2055 100644 --- a/server/core/lib/user-import-export/user-importer.ts +++ b/server/core/lib/user-import-export/user-importer.ts @@ -1,5 +1,5 @@ import { UserImportResultSummary, UserImportState } from '@peertube/peertube-models' -import { getFilenameWithoutExt } from '@peertube/peertube-node-utils' +import { getFilenameWithoutExt, getFileSize } from '@peertube/peertube-node-utils' import { saveInTransactionWithRetries } from '@server/helpers/database-utils.js' import { logger, loggerTagsFactory } from '@server/helpers/logger.js' import { unzip } from '@server/helpers/unzip.js' @@ -20,6 +20,7 @@ import { UserVideoHistoryImporter } from './importers/user-video-history-importe import { VideoPlaylistsImporter } from './importers/video-playlists-importer.js' import { VideosImporter } from './importers/videos-importer.js' import { WatchedWordsListsImporter } from './importers/watched-words-lists-importer.js' +import { parseBytes } from '@server/helpers/core-utils.js' const lTags = loggerTagsFactory('user-import') @@ -51,7 +52,14 @@ export class UserImporter { const inputZip = getFSUserImportFilePath(importModel) this.extractedDirectory = join(dirname(inputZip), getFilenameWithoutExt(inputZip)) - await unzip(inputZip, this.extractedDirectory) + await unzip({ + source: inputZip, + destination: this.extractedDirectory, + // Videos that take a lot of space don't have a good compression ratio + // Keep a minimum of 1GB if the archive doesn't contain video files + maxSize: Math.max(await getFileSize(inputZip) * 2, parseBytes('1GB')), + maxFiles: 10000 + }) const user = await UserModel.loadByIdFull(importModel.userId)