diff --git a/packages/api/src/import/archive.ts b/packages/api/src/import/archive.ts index 7be7c564d5..0f929e016e 100644 --- a/packages/api/src/import/archive.ts +++ b/packages/api/src/import/archive.ts @@ -128,8 +128,20 @@ function indexEntries( return new Promise((resolve, reject) => { const index = new Map(); let total = 0; + /** Counted per record, not per indexed entry. `index.size` undercounts: + * directory records return before it is ever consulted, and a repeated + * filename overwrites the same key. Either lets an archive hold millions + * of records that cost real central-directory traversal while the cap + * believes it is nearly empty. */ + let records = 0; zipfile.on('entry', (entry: yauzl.Entry) => { + records += 1; + if (records > options.maxEntries) { + reject(new ZipBombError(`Archive exceeds ${options.maxEntries} entries`)); + return; + } + if (/\/$/.test(entry.fileName)) { zipfile.readEntry(); return; @@ -142,11 +154,6 @@ function indexEntries( return; } - if (index.size + 1 > options.maxEntries) { - reject(new ZipBombError(`Archive exceeds ${options.maxEntries} entries`)); - return; - } - /** Cheap early reject from the central directory's declared sizes. * This field is attacker-controlled and not trusted on its own — * `ArchiveTotals` re-checks the real, streamed byte count on every