actualbudget/actual · error · UnsafeZipError

Zip archive's total uncompressed size exceeds maximum of ${m

Error message

Zip archive's total uncompressed size exceeds maximum of ${maxTotalUncompressedSize} bytes

What it means

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.

Source

Thrown at packages/loot-core/src/server/util/zip.ts:79

  return unzipSync(data, {
    filter(file) {
      assertSafeEntryName(file.name);

      if (file.originalSize > maxEntrySize) {
        throw new UnsafeZipError(
          `Zip entry "${file.name}" exceeds maximum size of ${maxEntrySize} bytes`,
          {
            zipReason: 'entry-size',
            entryName: file.name,
            maxSize: maxEntrySize,
          },
        );
      }

      totalUncompressedSize += file.originalSize;
      if (totalUncompressedSize > maxTotalUncompressedSize) {
        throw new UnsafeZipError(
          `Zip archive's total uncompressed size exceeds maximum of ${maxTotalUncompressedSize} bytes`,
          { zipReason: 'total-size', maxSize: maxTotalUncompressedSize },
        );
      }

      const normalized = file.name.toLowerCase();
      if (seen.has(normalized)) {
        throw new UnsafeZipError(
          `Zip archive contains a duplicate entry: ${file.name}`,
          { zipReason: 'duplicate-entry', entryName: file.name },
        );
      }
      seen.add(normalized);

      return true;
    },
  });
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Prune old attachments or split the backup into smaller archives to reduce total uncompressed size.
  2. Raise maxTotalUncompressedSize in the safeUnzip options if your deployment has the memory/disk for it.
  3. Track cumulative extraction size in your own pipeline and stream entries to disk instead of holding them in memory.
  4. On untrusted input, reject the archive outright — the total-size check exists to stop resource exhaustion.

Example fix

// before
const entries = safeUnzip(backupBuffer);
// after
const entries = safeUnzip(backupBuffer, {
  maxTotalUncompressedSize: 500 * 1024 * 1024,
});
Defensive patterns

Strategy: try-catch

Validate before calling

// Estimate total size from a pre-scan of the central directory; if unavailable, cap the compressed input:
if (buf.length > MAX_ARCHIVE) throw new Error('Refusing to extract oversized archive');

Try / catch

try {
  const entries = safeUnzip(buf);
} catch (e) {
  if (e instanceof UnsafeZipError && e.zipReason === 'total-size') {
    showError(`Archive contents exceed ${e.maxSize} bytes when decompressed`);
  } else { throw e; }
}

Prevention

When it happens

Trigger: 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.

Common situations: Long-lived budgets accumulating hundreds of attachments until the total backup exceeds the limit; nested/recursive zip bombs (zips containing zips) expanding multiplicatively.

Related errors


AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29). Data as JSON: /api/errors/1d1fdbdc1fce3375. Report an issue: GitHub.