Limit total extracted size when installing zips (zip bomb protection)

ExtractZipContents wrote every entry's declared size with no ceiling, so
a small high-ratio zip could decompress to fill the storage device.

- Add a maxTotalSize parameter to ExtractZipContents (default 4GB) and
  ExtractFile.
- Bail out in the size-summation pass when the total declared size
  exceeds the limit, and again per write chunk in case declared sizes
  are inaccurate.
This commit is contained in:
Henrik Rydgård committed 2026-08-01 13:16:21 +02:00
1 parent 6d43b6f531
commit 725cd691b3
2 files changed
+24 -6

No files matched your search

+21 -4
View File
@@ -562,7 +562,7 @@ std::string GameManager::GetISOGameID(FileLoader *loader) const {
return sfo.GetValueString("DISC_ID");
}
bool GameManager::ExtractFile(struct zip *z, int file_index, const Path &outFilename, size_t *bytesCopied, size_t allBytes) {
bool GameManager::ExtractFile(struct zip *z, int file_index, const Path &outFilename, size_t *bytesCopied, size_t allBytes, size_t maxTotalSize) {
struct zip_stat zstat;
zip_stat_index(z, file_index, 0, &zstat);
size_t size = zstat.size;
@@ -583,6 +583,16 @@ bool GameManager::ExtractFile(struct zip *z, int file_index, const Path &outFile
u8 *buffer = new u8[blockSize];
while (pos < size) {
size_t readSize = std::min(blockSize, size - pos);
// Stop before the total would exceed the limit (zip bomb), even
// if the declared sizes in the archive were inaccurate.
if (*bytesCopied > maxTotalSize || readSize > maxTotalSize - *bytesCopied) {
ERROR_LOG(Log::HLE, "Bailing: zip contents too large, limit %d", (int)maxTotalSize);
delete[] buffer;
fclose(f);
zip_fclose(zf);
File::Delete(outFilename);
return false;
}
zip_int64_t retval = zip_fread(zf, buffer, readSize);
if (retval < 0 || (size_t)retval < readSize) {
ERROR_LOG(Log::HLE, "Failed to read %d bytes from zip (%d) - archive corrupt?", (int)readSize, (int)retval);
@@ -625,7 +635,7 @@ bool GameManager::ExtractFile(struct zip *z, int file_index, const Path &outFile
}
// Doesn't care what it is, just extracts the whole ZIP to the requested location.
bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipFileInfo &info, bool allowRoot) {
bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipFileInfo &info, bool allowRoot, size_t maxTotalSize) {
size_t allBytes = 0;
size_t bytesCopied = 0;
@@ -659,6 +669,7 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF
// Create all the directories first in one pass
std::set<Path> createdDirs;
std::vector<Path> createdFiles;
for (int i = 0; i < info.numFiles; i++) {
// Let's count the directories as the first 10%.
const char *fn = zip_get_name(z, i, 0);
@@ -688,6 +699,13 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF
if (!isDir && fileAllowed(fn)) {
struct zip_stat zstat;
if (zip_stat_index(z, i, 0, &zstat) >= 0) {
// Guard against zip bombs: the total declared size must not
// exceed the limit. Check before adding to avoid overflow.
if (zstat.size > maxTotalSize || allBytes > maxTotalSize - zstat.size) {
ERROR_LOG(Log::HLE, "Bailing: zip contents too large (%d bytes), limit %d", (int)allBytes, (int)maxTotalSize);
SetInstallError(sy->T("Too large"));
goto bail;
}
allBytes += zstat.size;
}
}
@@ -697,7 +715,6 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF
INFO_LOG(Log::HLE, "Created %d directories", (int)createdDirs.size());
// Now, loop through again in a second pass, writing files.
std::vector<Path> createdFiles;
for (int i = 0; i < info.numFiles; i++) {
const char *fn = zip_get_name(z, i, 0);
// Note that we do NOT write files that are not in a directory, to avoid random
@@ -710,7 +727,7 @@ bool GameManager::ExtractZipContents(struct zip *z, const Path &dest, const ZipF
if (isDir)
continue;
if (!ExtractFile(z, i, outFilename, &bytesCopied, allBytes)) {
if (!ExtractFile(z, i, outFilename, &bytesCopied, allBytes, maxTotalSize)) {
ERROR_LOG(Log::HLE, "Bailing: Failed to extract file: %s -> %s", zippedName.c_str(), outFilename.c_str());
goto bail;
} else {
+3 -2
View File
@@ -89,7 +89,8 @@ public:
bool UninstallGameOnThread(const std::string &name);
// Extracts the contents of an open zip archive into dest. Exposed for testing.
bool ExtractZipContents(struct zip *z, const Path &dest, const ZipFileInfo &info, bool allowRoot);
// maxTotalSize limits the total decompressed size (zip bomb protection).
bool ExtractZipContents(struct zip *z, const Path &dest, const ZipFileInfo &info, bool allowRoot, size_t maxTotalSize = 0x100000000);
private:
void InstallZipContents(ZipFileTask task);
@@ -99,7 +100,7 @@ private:
void InstallDone();
bool ExtractFile(struct zip *z, int file_index, const Path &outFilename, size_t *bytesCopied, size_t allBytes);
bool ExtractFile(struct zip *z, int file_index, const Path &outFilename, size_t *bytesCopied, size_t allBytes, size_t maxTotalSize = 0x100000000);
bool DetectTexturePackDest(struct zip *z, int iniIndex, Path &dest);
void SetInstallError(std::string_view err);