From fc57538611b58d655d2d6cae779d8ece50244dd9 Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Wed, 29 Jul 2026 05:14:13 +0200 Subject: [PATCH] fix(import): count every zip record against the entry cap index.size undercounted it: directory records return before the check, and a repeated filename overwrites the same map key. Either lets an archive carry far more records than the cap allows, each costing real central-directory traversal, while the limit believes the archive is nearly empty. --- packages/api/src/import/archive.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) 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