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
- Open the zip with an archive tool and remove/rename duplicate entries so every (case-insensitive) name is unique
- Regenerate the archive from source files instead of merging existing zips
- If legitimate case-differing names are needed, rename one entry (zip consumers on case-insensitive filesystems cannot hold both anyway)
- 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
- Rebuild archives from extracted files instead of merging zips
- Run `zipinfo`/`unzip -l` and check for case-insensitive duplicate names before importing
- Never hand-edit zip entry names to differ only by case
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
- Unsafe zip entry name: ${name}
- zipMeta ? getUnsafeZipError(zipMeta) : error
- Invalid font-family value for "${property}": function calls
- Invalid font src: only data: URIs are allowed in @font-face.
- Theme CSS contains forbidden at-rules (@import, @media, @key
AI-assisted analysis of actualbudget/actual@d4334cb6e6 (2026-08-29).
Data as JSON: /api/errors/daa0444fdb8192d3.
Report an issue: GitHub.