From ba225c1e301aafd1a637231245d5f159dfc92426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yunsup=20Sim=28=EC=8B=AC=EC=9C=A4=EC=84=AD=29?= <47362439+SimYunSup@users.noreply.github.com> Date: Tue, 4 Aug 2026 18:34:00 +0900 Subject: [PATCH] fix(zip): guard zip64 extra field parsing against malformed input --- modules/zip/src/parse-zip/cd-file-header.ts | 43 +++++----- .../zip/test/zip-utils/cd-file-header.spec.ts | 79 +++++++++++++++++++ 2 files changed, 101 insertions(+), 21 deletions(-) diff --git a/modules/zip/src/parse-zip/cd-file-header.ts b/modules/zip/src/parse-zip/cd-file-header.ts index a7529341e0..3579039bf8 100644 --- a/modules/zip/src/parse-zip/cd-file-header.ts +++ b/modules/zip/src/parse-zip/cd-file-header.ts @@ -57,6 +57,7 @@ const CD_EXTRA_FIELD_LENGTH_OFFSET = 30; const CD_START_DISK_OFFSET = 32; const CD_LOCAL_HEADER_OFFSET_OFFSET = 42; const CD_FILE_NAME_OFFSET = 46n; +const ZIP64_EXTRA_FIELD_ID = 0x0001; export const signature: ZipSignature = new Uint8Array([0x50, 0x4b, 0x01, 0x02]); @@ -144,14 +145,6 @@ export async function* makeZipCDHeaderIterator( ); } } -/** - * returns the number written in the provided bytes - * @param bytes two bytes containing the number - * @returns the number written in the provided bytes - */ -const getUint16 = (...bytes: [number, number]) => { - return bytes[0] + bytes[1] * 16; -}; /** * reads all nesessary data from zip64 record in the extra data @@ -167,19 +160,27 @@ const findZip64DataInExtra = (zip64data: Zip64Data, extraField: DataView): Parti if (zip64dataList.length > 0) { // total length of data in zip64 notation in bytes const zip64chunkSize = zip64dataList.reduce((sum, curr) => sum + curr.length, 0); - // we're looking for the zip64 nontation header (0x0001) - // and a size field with a correct value next to it - const offsetInExtraData = new Uint8Array(extraField.buffer).findIndex( - (_val, i, arr) => - getUint16(arr[i], arr[i + 1]) === 0x0001 && - getUint16(arr[i + 2], arr[i + 3]) === zip64chunkSize - ); - // then we read all the nesessary fields from the zip64 data - let bytesRead = 0; - for (const note of zip64dataList) { - const offset = bytesRead; - zip64DataRes[note.name] = extraField.getBigUint64(offsetInExtraData + 4 + offset, true); - bytesRead = offset + note.length; + let offset = 0; + while (offset + 4 <= extraField.byteLength) { + const headerId = extraField.getUint16(offset, true); + const dataSize = extraField.getUint16(offset + 2, true); + const payloadStart = offset + 4; + if (payloadStart + dataSize > extraField.byteLength) { + break; + } + if (headerId === ZIP64_EXTRA_FIELD_ID && dataSize === zip64chunkSize) { + let bytesRead = 0; + for (const note of zip64dataList) { + const fieldOffset = payloadStart + bytesRead; + if (fieldOffset + 8 > payloadStart + dataSize) { + break; + } + zip64DataRes[note.name] = extraField.getBigUint64(fieldOffset, true); + bytesRead += note.length; + } + break; + } + offset = payloadStart + dataSize; } } diff --git a/modules/zip/test/zip-utils/cd-file-header.spec.ts b/modules/zip/test/zip-utils/cd-file-header.spec.ts index 7af260c7f0..1d42fd8615 100644 --- a/modules/zip/test/zip-utils/cd-file-header.spec.ts +++ b/modules/zip/test/zip-utils/cd-file-header.spec.ts @@ -39,3 +39,82 @@ test('SLPKLoader#zip64 info generation', async t => { t.equal(header.byteLength, 20); t.end(); }); + +test('SLPKLoader#central directory file header parse with zip64 sentinel and empty extra field', async t => { + const header = generateCDHeader({crc32: 0, fileName: 'test.json', length: 0, offset: 0n}); + const view = new DataView(header); + view.setUint32(20, 0xffffffff, true); + view.setUint32(24, 0xffffffff, true); + const cdFileHeader = await parseZipCDFileHeader(0n, new DataViewReadableFile(view)); + t.equal(cdFileHeader?.fileName, 'test.json'); + t.equal(cdFileHeader?.uncompressedSize, BigInt(0xffffffff)); + t.equal(cdFileHeader?.compressedSize, BigInt(0xffffffff)); + t.equal(cdFileHeader?.extraFieldLength, 0); + t.end(); +}); + +test('SLPKLoader#central directory file header parse with truncated zip64 extra field', async t => { + const header = generateCDHeader({crc32: 0, fileName: 'test.json', length: 0, offset: 0n}); + const view = new DataView(header); + view.setUint32(20, 0xffffffff, true); + view.setUint32(24, 0xffffffff, true); + view.setUint16(30, 8, true); + const extra = new Uint8Array([0x01, 0x00, 0x10, 0x00, 0, 0, 0, 0]); + const buffer = new Uint8Array(header.byteLength + extra.byteLength); + buffer.set(new Uint8Array(header), 0); + buffer.set(extra, header.byteLength); + const cdFileHeader = await parseZipCDFileHeader( + 0n, + new DataViewReadableFile(new DataView(buffer.buffer)) + ); + t.equal(cdFileHeader?.uncompressedSize, BigInt(0xffffffff)); + t.equal(cdFileHeader?.fileName, 'test.json'); + t.equal(cdFileHeader?.extraFieldLength, 8); + t.end(); +}); + +test('SLPKLoader#central directory file header parse with valid zip64 extra field', async t => { + const header = generateCDHeader({ + crc32: 0, + fileName: 'test.json', + length: 0xffffffffff, + offset: 0n + }); + const cdFileHeader = await parseZipCDFileHeader( + 0n, + new DataViewReadableFile(new DataView(header)) + ); + t.equal(cdFileHeader?.uncompressedSize, BigInt(0xffffffffff)); + t.equal(cdFileHeader?.compressedSize, BigInt(0xffffffffff)); + t.equal(cdFileHeader?.extraFieldLength, 20); + t.equal(cdFileHeader?.localHeaderOffset, 0n); + t.end(); +}); + +test('SLPKLoader#central directory file header parse with zip64 extra field after unrelated record', async t => { + const header = generateCDHeader({crc32: 0, fileName: 'test.json', length: 0, offset: 0n}); + const view = new DataView(header); + view.setUint32(20, 0xffffffff, true); + view.setUint32(24, 0xffffffff, true); + view.setUint16(30, 28, true); + const extra = new DataView(new ArrayBuffer(28)); + extra.setUint16(0, 0x9999, true); + extra.setUint16(2, 4, true); + extra.setUint16(4, 0x0001, true); + extra.setUint16(6, 16, true); + extra.setUint16(8, 0x0001, true); + extra.setUint16(10, 16, true); + extra.setBigUint64(12, BigInt(0x1122334455), true); + extra.setBigUint64(20, BigInt(0x66778899aa), true); + const buffer = new Uint8Array(header.byteLength + extra.byteLength); + buffer.set(new Uint8Array(header), 0); + buffer.set(new Uint8Array(extra.buffer), header.byteLength); + const cdFileHeader = await parseZipCDFileHeader( + 0n, + new DataViewReadableFile(new DataView(buffer.buffer)) + ); + t.equal(cdFileHeader?.uncompressedSize, BigInt(0x1122334455)); + t.equal(cdFileHeader?.compressedSize, BigInt(0x66778899aa)); + t.equal(cdFileHeader?.extraFieldLength, 28); + t.end(); +});