different-ai/openwork · error · Error

ZIP central directory entry is out of bounds.

Error message

ZIP central directory entry is out of bounds.

What it means

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.

Source

Thrown at apps/server/src/opencode-plugins/openwork-office-attachments.ts:294

  if (count > MAX_ZIP_ENTRIES) throw new Error(`ZIP entry count ${count} exceeds limit ${MAX_ZIP_ENTRIES}.`);
  if (centralOffset + centralSize > buffer.byteLength) throw new Error("ZIP central directory is out of bounds.");
  if (centralEnd > eocd) throw new Error("ZIP central directory overlaps the end-of-central-directory record.");

  const entries: ZipEntry[] = [];
  let cursor = centralOffset;
  let totalUncompressed = 0;
  for (let index = 0; index < count; index += 1) {
    if (cursor + 46 > centralEnd || buffer.readUInt32LE(cursor) !== ZIP_CENTRAL_DIRECTORY_HEADER) throw new Error("Invalid ZIP central directory entry.");
    const flags = buffer.readUInt16LE(cursor + 8);
    const method = buffer.readUInt16LE(cursor + 10);
    const compressedSize = buffer.readUInt32LE(cursor + 20);
    const uncompressedSize = buffer.readUInt32LE(cursor + 24);
    const nameLength = buffer.readUInt16LE(cursor + 28);
    const extraLength = buffer.readUInt16LE(cursor + 30);
    const commentLength = buffer.readUInt16LE(cursor + 32);
    const localOffset = buffer.readUInt32LE(cursor + 42);
    if (compressedSize === 0xffffffff || uncompressedSize === 0xffffffff || localOffset === 0xffffffff) throw new Error("ZIP64 archives are not supported.");
    if (cursor + 46 + nameLength + extraLength + commentLength > centralEnd) throw new Error("ZIP central directory entry is out of bounds.");
    const name = buffer.toString("utf8", cursor + 46, cursor + 46 + nameLength);
    rejectUnsafeZipFlags(flags, name);
    if (method !== ZIP_STORED && method !== ZIP_DEFLATE) throw new Error(`ZIP entry ${name} uses unsupported compression method ${method}.`);
    if (uncompressedSize > MAX_ENTRY_UNCOMPRESSED_BYTES) throw new Error(`ZIP entry ${name} exceeds per-entry uncompressed limit.`);
    if (uncompressedSize > 0 && compressedSize === 0) throw new Error(`ZIP entry ${name} has an invalid compression ratio.`);
    if (compressedSize > 0 && uncompressedSize / compressedSize > MAX_ZIP_COMPRESSION_RATIO) throw new Error(`ZIP entry ${name} exceeds compression ratio limit.`);
    totalUncompressed += uncompressedSize;
    if (totalUncompressed > MAX_TOTAL_UNCOMPRESSED_BYTES) throw new Error("ZIP archive exceeds total uncompressed limit.");
    entries.push({ name, flags, method, compressedSize, uncompressedSize, localOffset });
    cursor += 46 + nameLength + extraLength + commentLength;
  }
  if (cursor !== centralEnd) throw new Error("ZIP central directory size does not match its entries.");
  return entries;
}

function readZipEntryData(buffer: Buffer, entry: ZipEntry): Buffer {
  const cursor = entry.localOffset;
  if (cursor + 30 > buffer.byteLength || buffer.readUInt32LE(cursor) !== ZIP_LOCAL_FILE_HEADER) throw new Error(`Invalid local ZIP header for ${entry.name}.`);

View on GitHub (pinned to 2b7df46e8a)

Solutions

  1. Obtain a fresh, complete copy of the file (re-download/re-export it).
  2. Verify the file with 'unzip -t file' or an integrity check to confirm corruption.
  3. Re-save/export the document from the originating application to regenerate a valid ZIP.
  4. If generating ZIPs yourself, use a standard library and do not hand-patch header length fields.

Example fix

// before
const entries = await parseZip(truncatedBuffer) // throws out-of-bounds
// after
if (!buffer.subarray(0, 4).equals(Buffer.from('PK\x03\x04')) || buffer.length < END_HEADER_MIN) {
  throw new Error('Not a complete ZIP file')
}
const entries = await parseZip(buffer)
Defensive patterns

Strategy: validation

Validate before calling

function looksLikeCompleteZip(buf: Buffer): boolean {
  const eocd = buf.lastIndexOf(Buffer.from('PK\x05\x06'))
  return buf.length >= 22 && eocd !== -1 && buf.subarray(0, 2).toString() === 'PK'
}
if (!looksLikeCompleteZip(fileBuffer)) throw new Error('truncated or non-ZIP file')

Type guard

function isZipBuffer(v: unknown): v is Buffer {
  return v instanceof Buffer && v.length >= 4 && v[0] === 0x50 && v[1] === 0x4b
}

Try / catch

try {
  const entries = await extractOfficeAttachments(file)
} catch (e) {
  if (e instanceof Error && e.message === 'ZIP central directory entry is out of bounds.') {
    quarantineFile(file.name); alertUser('file is corrupt — request a fresh copy')
  } else throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01). Data as JSON: /api/errors/f2778fd01bcdb763. Report an issue: GitHub.