UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

81 lines (80 loc) • 3.53 kB
/** * Bounded ZIP entry reading, shared by every processor that opens a ZIP. * * Extracted from ArchiveProcessor because the Office formats are ZIPs too: * .pptx and .odt read their entries directly and so can be handed the same * bomb, and need the same refusal. One implementation rather than three means * a correction to the guard lands everywhere at once. * * .docx and .xlsx deliberately do NOT use this. mammoth and exceljs unzip for * themselves and were measured refusing a 400MB bomb at 46MB and 53MB peak, * so wrapping them in a pre-scan bought no safety and roughly tripled the cost * of every ordinary document. * * @module processors/archive/zipEntryReader */ /** ZIP compression methods this reader handles (APPNOTE 4.4.5). */ export const ZIP_METHOD_STORED = 0; export const ZIP_METHOD_DEFLATED = 8; /** * Whether a zlib rejection is the output bound firing rather than bad input. * * `maxOutputLength` aborts an inflate the moment its output would pass the cap, * which is the whole point — but it surfaces as a plain `RangeError`, and a * bomb reported as "failed to decompress" reads as a corrupt upload and invites * the user to send it again. It will fail identically every time. * * Keyed on `code`, not the message: the message embeds a byte count. */ export const isDecompressionBoundExceeded = (error) => error?.code === "ERR_BUFFER_TOO_LARGE"; /** * Read one ZIP entry's bytes without trusting the size it declares. * * `entry.getData()` cannot be used for this. It sizes its output buffer from * the central-directory `size` field, which the archive author chooses, and * adm-zip only arms its own guard when that field is positive: * * const option = version >= 15 && expectedLength > 0 * ? { maxOutputLength: expectedLength } : {}; * * So an entry declaring 0 disables the bound and the caller's `size > maxSize` * check in one move — `0 > 5MB` is false, and the inflate then runs uncapped. * The declared size is the attack, so nothing here may depend on it: the cap * comes from our own limit and is handed to the decoder. * * CRC is verified on both paths rather than dropped, so bypassing `getData()` * does not also quietly lose its corruption check — a STORED entry is copied * out rather than decoded, but it can be damaged just the same. It detects * damage, not malice — the CRC field is attacker-controlled too. */ export function readZipEntryWithinLimit(entry, maxBytes, zlibModule) { const compressed = entry.getCompressedData(); const matchesCrc = (data) => (zlibModule.crc32(data) >>> 0) === (entry.header.crc >>> 0); // STORED: the bytes are already the payload, so its own length is the bound. if (entry.header.method === ZIP_METHOD_STORED) { if (compressed.length > maxBytes) { return { status: "too-large" }; } return matchesCrc(compressed) ? { status: "ok", buffer: compressed } : { status: "corrupt" }; } if (entry.header.method !== ZIP_METHOD_DEFLATED) { return { status: "unsupported-method" }; } let inflated; try { inflated = zlibModule.inflateRawSync(compressed, { maxOutputLength: maxBytes, }); } catch (error) { if (isDecompressionBoundExceeded(error)) { return { status: "too-large" }; } return { status: "corrupt" }; } return matchesCrc(inflated) ? { status: "ok", buffer: inflated } : { status: "corrupt" }; }