{"record":{"id":"f2778fd01bcdb763","repo":"different-ai/openwork","slug":"zip-central-directory-entry-is-out-of-bounds","errorCode":null,"errorMessage":"ZIP central directory entry is out of bounds.","messagePattern":"ZIP central directory entry is out of bounds\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/server/src/opencode-plugins/openwork-office-attachments.ts","lineNumber":294,"sourceCode":"  if (count > MAX_ZIP_ENTRIES) throw new Error(`ZIP entry count ${count} exceeds limit ${MAX_ZIP_ENTRIES}.`);\n  if (centralOffset + centralSize > buffer.byteLength) throw new Error(\"ZIP central directory is out of bounds.\");\n  if (centralEnd > eocd) throw new Error(\"ZIP central directory overlaps the end-of-central-directory record.\");\n\n  const entries: ZipEntry[] = [];\n  let cursor = centralOffset;\n  let totalUncompressed = 0;\n  for (let index = 0; index < count; index += 1) {\n    if (cursor + 46 > centralEnd || buffer.readUInt32LE(cursor) !== ZIP_CENTRAL_DIRECTORY_HEADER) throw new Error(\"Invalid ZIP central directory entry.\");\n    const flags = buffer.readUInt16LE(cursor + 8);\n    const method = buffer.readUInt16LE(cursor + 10);\n    const compressedSize = buffer.readUInt32LE(cursor + 20);\n    const uncompressedSize = buffer.readUInt32LE(cursor + 24);\n    const nameLength = buffer.readUInt16LE(cursor + 28);\n    const extraLength = buffer.readUInt16LE(cursor + 30);\n    const commentLength = buffer.readUInt16LE(cursor + 32);\n    const localOffset = buffer.readUInt32LE(cursor + 42);\n    if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff || localOffset === 0xffffffff) throw new Error(\"ZIP64 archives are not supported.\");\n    if (cursor + 46 + nameLength + extraLength + commentLength > centralEnd) throw new Error(\"ZIP central directory entry is out of bounds.\");\n    const name = buffer.toString(\"utf8\", cursor + 46, cursor + 46 + nameLength);\n    rejectUnsafeZipFlags(flags, name);\n    if (method !== ZIP_STORED && method !== ZIP_DEFLATE) throw new Error(`ZIP entry ${name} uses unsupported compression method ${method}.`);\n    if (uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) throw new Error(`ZIP entry ${name} exceeds per-entry uncompressed limit.`);\n    if (uncompressedSize > 0 && compressedSize === 0) throw new Error(`ZIP entry ${name} has an invalid compression ratio.`);\n    if (compressedSize > 0 && uncompressedSize / compressedSize > MAX_ZIP_COMPRESSION_RATIO) throw new Error(`ZIP entry ${name} exceeds compression ratio limit.`);\n    totalUncompressed += uncompressedSize;\n    if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED_BYTES) throw new Error(\"ZIP archive exceeds total uncompressed limit.\");\n    entries.push({ name, flags, method, compressedSize, uncompressedSize, localOffset });\n    cursor += 46 + nameLength + extraLength + commentLength;\n  }\n  if (cursor !== centralEnd) throw new Error(\"ZIP central directory size does not match its entries.\");\n  return entries;\n}\n\nfunction readZipEntryData(buffer: Buffer, entry: ZipEntry): Buffer {\n  const cursor = entry.localOffset;\n  if (cursor + 30 > buffer.byteLength || buffer.readUInt32LE(cursor) !== ZIP_LOCAL_FILE_HEADER) throw new Error(`Invalid local ZIP header for ${entry.name}.`);","sourceCodeStart":276,"sourceCodeEnd":312,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/opencode-plugins/openwork-office-attachments.ts#L276-L312","documentation":"During ZIP central-directory parsing, listZipEntries validates that each 46-byte fixed header plus variable-length name/extra/comment fields fits within the directory bounds (centralEnd). If an entry's declared lengths overrun the directory, the archive is structurally corrupt or malicious and is rejected before any entry data is read.","triggerScenarios":"Opening an office attachment (docx/xlsx/pptx-like ZIP) whose central directory entry claims nameLength/extraLength/commentLength that extend past the end of the central directory — truncated downloads, hand-crafted zips, or zip-slip style fuzzing inputs.","commonSituations":"File truncated by a failed upload/download; archive edited or corrupted in transit; deliberately crafted ZIP intended to crash parsers; mixing offsets after binary-patching a ZIP.","solutions":["Obtain a fresh, complete copy of the file (re-download/re-export it).","Verify the file with 'unzip -t file' or an integrity check to confirm corruption.","Re-save/export the document from the originating application to regenerate a valid ZIP.","If generating ZIPs yourself, use a standard library and do not hand-patch header length fields."],"exampleFix":"// before\nconst entries = await parseZip(truncatedBuffer) // throws out-of-bounds\n// after\nif (!buffer.subarray(0, 4).equals(Buffer.from('PK\\x03\\x04')) || buffer.length < END_HEADER_MIN) {\n  throw new Error('Not a complete ZIP file')\n}\nconst entries = await parseZip(buffer)","handlingStrategy":"validation","validationCode":"function looksLikeCompleteZip(buf: Buffer): boolean {\n  const eocd = buf.lastIndexOf(Buffer.from('PK\\x05\\x06'))\n  return buf.length >= 22 && eocd !== -1 && buf.subarray(0, 2).toString() === 'PK'\n}\nif (!looksLikeCompleteZip(fileBuffer)) throw new Error('truncated or non-ZIP file')","typeGuard":"function isZipBuffer(v: unknown): v is Buffer {\n  return v instanceof Buffer && v.length >= 4 && v[0] === 0x50 && v[1] === 0x4b\n}","tryCatchPattern":"try {\n  const entries = await extractOfficeAttachments(file)\n} catch (e) {\n  if (e instanceof Error && e.message === 'ZIP central directory entry is out of bounds.') {\n    quarantineFile(file.name); alertUser('file is corrupt — request a fresh copy')\n  } else throw e\n}","preventionTips":["Verify file size/checksum after downloads before parsing","Reject files whose size does not match Content-Length","Never parse user-uploaded ZIPs without structural validation","Re-export documents from the source application when extraction fails"],"tags":["zip","file-parsing","security","corrupt-file"],"backgroundTag":"corrupt-zip-archive","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}