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
- 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.
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
- 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
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
- ZIP entry ${name} uses unsupported compression method ${meth
- ZIP data for ${entry.name} is out of bounds.
- ZIP uncompressed size mismatch for ${entry.name}.
- Archive entry exceeds the ${MAX_ENTRY_UNCOMPRESSED_BYTES}-by
- Workspace archive contains too much uncompressed data.
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/f2778fd01bcdb763.
Report an issue: GitHub.