{"record":{"id":"1d1fdbdc1fce3375","repo":"actualbudget/actual","slug":"zip-archive-s-total-uncompressed-size-exceeds-maxi","errorCode":null,"errorMessage":"Zip archive's total uncompressed size exceeds maximum of ${maxTotalUncompressedSize} bytes","messagePattern":"Zip archive's total uncompressed size exceeds maximum of (.+?) bytes","errorType":"exception","errorClass":"UnsafeZipError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/util/zip.ts","lineNumber":79,"sourceCode":"\n  return unzipSync(data, {\n    filter(file) {\n      assertSafeEntryName(file.name);\n\n      if (file.originalSize > maxEntrySize) {\n        throw new UnsafeZipError(\n          `Zip entry \"${file.name}\" exceeds maximum size of ${maxEntrySize} bytes`,\n          {\n            zipReason: 'entry-size',\n            entryName: file.name,\n            maxSize: maxEntrySize,\n          },\n        );\n      }\n\n      totalUncompressedSize += file.originalSize;\n      if (totalUncompressedSize > maxTotalUncompressedSize) {\n        throw new UnsafeZipError(\n          `Zip archive's total uncompressed size exceeds maximum of ${maxTotalUncompressedSize} bytes`,\n          { zipReason: 'total-size', maxSize: maxTotalUncompressedSize },\n        );\n      }\n\n      const normalized = file.name.toLowerCase();\n      if (seen.has(normalized)) {\n        throw new UnsafeZipError(\n          `Zip archive contains a duplicate entry: ${file.name}`,\n          { zipReason: 'duplicate-entry', entryName: file.name },\n        );\n      }\n      seen.add(normalized);\n\n      return true;\n    },\n  });\n}","sourceCodeStart":61,"sourceCodeEnd":97,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/util/zip.ts#L61-L97","documentation":"The filter callback accumulates each entry's uncompressed size and throws UnsafeZipError with zipReason 'total-size' once the running total exceeds maxTotalUncompressedSize. This caps the aggregate decompressed output of the whole archive, complementing the per-entry limit.","triggerScenarios":"A zip with many individually-small entries whose combined uncompressed size exceeds maxTotalUncompressedSize — many-attachment backup archives, or bomb archives with thousands of small entries crafted to evade per-entry checks.","commonSituations":"Long-lived budgets accumulating hundreds of attachments until the total backup exceeds the limit; nested/recursive zip bombs (zips containing zips) expanding multiplicatively.","solutions":["Prune old attachments or split the backup into smaller archives to reduce total uncompressed size.","Raise maxTotalUncompressedSize in the safeUnzip options if your deployment has the memory/disk for it.","Track cumulative extraction size in your own pipeline and stream entries to disk instead of holding them in memory.","On untrusted input, reject the archive outright — the total-size check exists to stop resource exhaustion."],"exampleFix":"// before\nconst entries = safeUnzip(backupBuffer);\n// after\nconst entries = safeUnzip(backupBuffer, {\n  maxTotalUncompressedSize: 500 * 1024 * 1024,\n});","handlingStrategy":"try-catch","validationCode":"// Estimate total size from a pre-scan of the central directory; if unavailable, cap the compressed input:\nif (buf.length > MAX_ARCHIVE) throw new Error('Refusing to extract oversized archive');","typeGuard":null,"tryCatchPattern":"try {\n  const entries = safeUnzip(buf);\n} catch (e) {\n  if (e instanceof UnsafeZipError && e.zipReason === 'total-size') {\n    showError(`Archive contents exceed ${e.maxSize} bytes when decompressed`);\n  } else { throw e; }\n}","preventionTips":["Prune attachments periodically to keep backups under the total-size cap","Set maxTotalUncompressedSize deliberately based on deployment memory/disk","Never disable the total-size check for untrusted input; it stops nested zip bombs"],"tags":["zip","security","size-limit","decompression-bomb"],"backgroundTag":"archive-total-size-exceeded","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}