actualbudget/actual · error · UnsafeZipError

Zip archive contains a duplicate entry: ${file.name}

Error message

Zip archive contains a duplicate entry: ${file.name}

What it means

loot-core's zip utility refuses to open a zip archive that contains two entries whose names are identical after lowercasing. This is a zip-slip/safety check implemented via UnsafeZipError to prevent ambiguous or malicious archives from silently overwriting files during import.

Source

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

          {
            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;
    },
  });
}

export function safeZip(files: Record<string, Uint8Array>): Uint8Array {
  for (const name of Object.keys(files)) {
    assertSafeEntryName(name);
  }
  return zipSync(files);
}

View on GitHub (pinned to d4334cb6e6)

Solutions

  1. Open the zip with an archive tool and remove/rename duplicate entries so every (case-insensitive) name is unique
  2. Regenerate the archive from source files instead of merging existing zips
  3. If legitimate case-differing names are needed, rename one entry (zip consumers on case-insensitive filesystems cannot hold both anyway)
  4. If you believe a valid archive is rejected, report upstream, but avoid disabling the safety check

Example fix

// before: archive holds 'budget.json' and 'Budget.json'
$ zipinfo bad.zip | sort -f | uniq -di  # find duplicates
// after
$ zip bad.zip -d Budget.json  # or rebuild: zip -r clean.zip ./extracted
Defensive patterns

Strategy: validation

Validate before calling

// client-side pre-check after reading entries
const names = entries.map(e => e.name.toLowerCase());
const dupes = names.filter((n, i) => names.indexOf(n) !== i);
if (dupes.length) throw new Error(`Duplicate zip entries: ${dupes.join(', ')}`);

Try / catch

try {
  await importZip(file);
} catch (e) {
  if (e instanceof UnsafeZipError && e.details?.zipReason === 'duplicate-entry') {
    notify(`Archive contains duplicate entry "${e.details.entryName}" — please rebuild the zip`);
  } else throw e;
}

Prevention

When it happens

Trigger: Calling the zip import/extract path (maxTotalUncompressedSize check region) with an archive where the same entry name appears twice, differing only by case (e.g. 'data.json' and 'Data.json'), since names are normalized to lowercase before the seen-set check.

Common situations: Re-exported or hand-assembled zip files, archives merged from multiple budgets, or zips produced by tools that allow duplicate entries; also archives crafted maliciously to bypass path-based extraction.

Related errors


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