{"record":{"id":"daa0444fdb8192d3","repo":"actualbudget/actual","slug":"zip-archive-contains-a-duplicate-entry-file-nam","errorCode":null,"errorMessage":"Zip archive contains a duplicate entry: ${file.name}","messagePattern":"Zip archive contains a duplicate entry: (.+?)","errorType":"exception","errorClass":"UnsafeZipError","httpStatus":null,"severity":"error","filePath":"packages/loot-core/src/server/util/zip.ts","lineNumber":87,"sourceCode":"          {\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}\n\nexport function safeZip(files: Record<string, Uint8Array>): Uint8Array {\n  for (const name of Object.keys(files)) {\n    assertSafeEntryName(name);\n  }\n  return zipSync(files);\n}\n","sourceCodeStart":69,"sourceCodeEnd":105,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/loot-core/src/server/util/zip.ts#L69-L105","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: archive holds 'budget.json' and 'Budget.json'\n$ zipinfo bad.zip | sort -f | uniq -di  # find duplicates\n// after\n$ zip bad.zip -d Budget.json  # or rebuild: zip -r clean.zip ./extracted\n","handlingStrategy":"validation","validationCode":"// client-side pre-check after reading entries\nconst names = entries.map(e => e.name.toLowerCase());\nconst dupes = names.filter((n, i) => names.indexOf(n) !== i);\nif (dupes.length) throw new Error(`Duplicate zip entries: ${dupes.join(', ')}`);","typeGuard":null,"tryCatchPattern":"try {\n  await importZip(file);\n} catch (e) {\n  if (e instanceof UnsafeZipError && e.details?.zipReason === 'duplicate-entry') {\n    notify(`Archive contains duplicate entry \"${e.details.entryName}\" — please rebuild the zip`);\n  } else throw e;\n}","preventionTips":["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"],"tags":["zip","import","validation","security"],"backgroundTag":"duplicate-zip-entry","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}