Skip to content

Commit 8065d3c

Browse files
committed
fix(file-parsers): read the JSZip view with JSZip's 32-bit integer semantics
JSZip's readInt folds 4- and 8-byte fields through 32-bit shifts, so it sees only the low dword as a signed int, and its signature probe throws on an out-of-range ZIP64 locator pointer rather than falling back. The JSZip-view check read full-width values, so a field with high bits set could make it conclude JSZip would refuse a buffer JSZip actually opens. Read those fields with readInt32LE and mirror the throw conditions. The guard's own walk keeps full-width reads, so any divergence between the two is rejected. The single-use ZIP64 record helper is inlined back into locateCentralDirectory.
1 parent 556c786 commit 8065d3c

1 file changed

Lines changed: 34 additions & 42 deletions

File tree

‎apps/sim/lib/file-parsers/zip-guard.ts‎

Lines changed: 34 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -143,30 +143,6 @@ function eocdUsesZip64(buffer: Buffer, eocdOffset: number): boolean {
143143
)
144144
}
145145

146-
interface Zip64EocdRecord {
147-
entryCount: number
148-
centralDirectorySize: number
149-
centralDirectoryOffset: number
150-
/** Bytes from the record's signature through its last field. */
151-
recordLength: number
152-
}
153-
154-
function readZip64EocdRecord(buffer: Buffer, recordOffset: number): Zip64EocdRecord | null {
155-
if (
156-
recordOffset + 56 > buffer.length ||
157-
!hasSignatureAt(buffer, recordOffset, ZIP64_EOCD_SIGNATURE)
158-
) {
159-
return null
160-
}
161-
return {
162-
entryCount: Number(buffer.readBigUInt64LE(recordOffset + 32)),
163-
centralDirectorySize: Number(buffer.readBigUInt64LE(recordOffset + 40)),
164-
centralDirectoryOffset: Number(buffer.readBigUInt64LE(recordOffset + 48)),
165-
// The size field excludes the leading signature (4) and itself (8).
166-
recordLength: 12 + Number(buffer.readBigUInt64LE(recordOffset + 4)),
167-
}
168-
}
169-
170146
interface ZipParserView {
171147
eocdOffset: number
172148
/** Bytes JSZip treats as prepended data and rebases every offset past. */
@@ -180,40 +156,53 @@ interface ZipParserView {
180156
* reads the archive from, mirroring `readEndOfCentral` in jszip 3.10. Unlike
181157
* the guard it does not anchor on the buffer tail: it takes the LAST EOCD
182158
* signature anywhere, the last ZIP64 locator signature anywhere (and the last
183-
* ZIP64 record signature if the locator's pointer misses), and treats any gap
184-
* before the central directory as prepended data. Returns `null` when JSZip
185-
* would refuse the buffer.
159+
* ZIP64 record signature if the locator's in-range pointer misses), and treats
160+
* any gap before the central directory as prepended data. Returns `null` when
161+
* JSZip would refuse the buffer.
162+
*
163+
* Fields are read with JSZip's integer semantics, not the spec's: its
164+
* `readInt` folds 4- and 8-byte values through 32-bit shifts, keeping only the
165+
* low dword as a signed int (`readInt32LE`). Reading the full width here would
166+
* let a value with high bits set look out of range while JSZip reads it as an
167+
* ordinary offset or size.
186168
*/
187169
function locateAsZipParser(buffer: Buffer): ZipParserView | null {
188170
const eocdOffset = buffer.lastIndexOf(EOCD_SIGNATURE_BYTES)
189171
if (eocdOffset < 0 || eocdOffset + EOCD_MIN_SIZE > buffer.length) {
190172
return null
191173
}
192174

193-
let centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + 16)
194-
let expectedEocdOffset = centralDirectoryOffset + buffer.readUInt32LE(eocdOffset + 12)
175+
let centralDirectoryOffset = buffer.readInt32LE(eocdOffset + 16)
176+
let expectedEocdOffset = centralDirectoryOffset + buffer.readInt32LE(eocdOffset + 12)
195177
if (eocdUsesZip64(buffer, eocdOffset)) {
196178
const locatorOffset = buffer.lastIndexOf(ZIP64_EOCD_LOCATOR_SIGNATURE_BYTES)
197179
if (locatorOffset < 0 || locatorOffset + ZIP64_EOCD_LOCATOR_SIZE > buffer.length) {
198180
return null
199181
}
200-
const pointedRecordOffset = Number(buffer.readBigUInt64LE(locatorOffset + 8))
201-
const record =
202-
readZip64EocdRecord(buffer, pointedRecordOffset) ??
203-
readZip64EocdRecord(buffer, buffer.lastIndexOf(ZIP64_EOCD_SIGNATURE_BYTES))
204-
if (!record) {
182+
// JSZip's signature probe throws on an out-of-range pointer instead of
183+
// falling back to the signature scan.
184+
const pointedRecordOffset = buffer.readInt32LE(locatorOffset + 8)
185+
if (pointedRecordOffset < 0 || pointedRecordOffset + 4 > buffer.length) {
205186
return null
206187
}
207-
centralDirectoryOffset = record.centralDirectoryOffset
188+
const recordOffset = hasSignatureAt(buffer, pointedRecordOffset, ZIP64_EOCD_SIGNATURE)
189+
? pointedRecordOffset
190+
: buffer.lastIndexOf(ZIP64_EOCD_SIGNATURE_BYTES)
191+
if (recordOffset < 0 || recordOffset + 56 > buffer.length) {
192+
return null
193+
}
194+
centralDirectoryOffset = buffer.readInt32LE(recordOffset + 48)
195+
// The record's size field (+4) excludes its leading signature and itself (12 bytes).
208196
expectedEocdOffset =
209197
centralDirectoryOffset +
210-
record.centralDirectorySize +
211-
record.recordLength +
198+
buffer.readInt32LE(recordOffset + 40) +
199+
12 +
200+
buffer.readInt32LE(recordOffset + 4) +
212201
ZIP64_EOCD_LOCATOR_SIZE
213202
}
214203

215204
const baseOffset = eocdOffset - expectedEocdOffset
216-
if (baseOffset < 0) {
205+
if (baseOffset < 0 || centralDirectoryOffset < 0) {
217206
return null
218207
}
219208
return { eocdOffset, baseOffset, centralDirectoryStart: baseOffset + centralDirectoryOffset }
@@ -267,13 +256,16 @@ export function locateCentralDirectory(
267256
return null
268257
}
269258

270-
const record = readZip64EocdRecord(buffer, Number(buffer.readBigUInt64LE(locatorOffset + 8)))
271-
if (!record) {
259+
const zip64EocdOffset = Number(buffer.readBigUInt64LE(locatorOffset + 8))
260+
if (
261+
zip64EocdOffset + 56 > buffer.length ||
262+
!hasSignatureAt(buffer, zip64EocdOffset, ZIP64_EOCD_SIGNATURE)
263+
) {
272264
return null
273265
}
274266

275-
entryCount = record.entryCount
276-
cdOffset = record.centralDirectoryOffset
267+
entryCount = Number(buffer.readBigUInt64LE(zip64EocdOffset + 32))
268+
cdOffset = Number(buffer.readBigUInt64LE(zip64EocdOffset + 48))
277269
}
278270

279271
if (cdOffset < 0 || cdOffset > buffer.length) {

0 commit comments

Comments
 (0)