{"record":{"id":"5b2a7a6779f53137","repo":"different-ai/openwork","slug":"zip-data-for-entry-name-is-out-of-bounds","errorCode":null,"errorMessage":"ZIP data for ${entry.name} is out of bounds.","messagePattern":"ZIP data for (.+?) is out of bounds\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/server/src/opencode-plugins/openwork-office-attachments.ts","lineNumber":326,"sourceCode":"}\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}.`);\n  const localFlags = buffer.readUInt16LE(cursor + 6);\n  const localMethod = buffer.readUInt16LE(cursor + 8);\n  const localCompressedSize = buffer.readUInt32LE(cursor + 18);\n  const localUncompressedSize = buffer.readUInt32LE(cursor + 22);\n  const nameLength = buffer.readUInt16LE(cursor + 26);\n  const extraLength = buffer.readUInt16LE(cursor + 28);\n  rejectUnsafeZipFlags(localFlags, entry.name);\n  if (localMethod !== entry.method) throw new Error(`ZIP method mismatch for ${entry.name}.`);\n  if (localCompressedSize !== entry.compressedSize || localUncompressedSize !== entry.uncompressedSize) throw new Error(`ZIP size mismatch for ${entry.name}.`);\n  if (cursor + 30 + nameLength + extraLength > buffer.byteLength) throw new Error(`ZIP local header for ${entry.name} is out of bounds.`);\n  const localName = buffer.toString(\"utf8\", cursor + 30, cursor + 30 + nameLength);\n  if (localName !== entry.name) throw new Error(`ZIP local header name mismatch for ${entry.name}.`);\n  const dataStart = cursor + 30 + nameLength + extraLength;\n  if (dataStart + entry.compressedSize > buffer.byteLength) throw new Error(`ZIP data for ${entry.name} is out of bounds.`);\n  const compressed = buffer.subarray(dataStart, dataStart + entry.compressedSize);\n  const data = entry.method === ZIP_STORED ? compressed : inflateRawSync(compressed);\n  if (data.byteLength !== entry.uncompressedSize) throw new Error(`ZIP uncompressed size mismatch for ${entry.name}.`);\n  return data;\n}\n\nfunction relevantXmlEntry(kind: OfficeKind, name: string): boolean {\n  if (!name.endsWith(\".xml\")) return false;\n  if (kind === \"docx\") {\n    return name === \"word/document.xml\"\n      || /^word\\/header\\d+\\.xml$/.test(name)\n      || /^word\\/footer\\d+\\.xml$/.test(name)\n      || name === \"word/footnotes.xml\"\n      || name === \"word/endnotes.xml\"\n      || name === \"word/comments.xml\";\n  }\n  return /^ppt\\/slides\\/slide\\d+\\.xml$/.test(name) || /^ppt\\/notesSlides\\/notesSlide\\d+\\.xml$/.test(name);\n}","sourceCodeStart":308,"sourceCodeEnd":344,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/opencode-plugins/openwork-office-attachments.ts#L308-L344","documentation":"Thrown while decompressing a ZIP local file entry inside an Office (docx/xlsx/pptx) attachment: the computed end of the entry's compressed data (dataStart + entry.compressedSize) exceeds the buffer's byte length. The parser validates every entry against the central directory, so this means the local record claims more data than the file physically contains — a truncated or corrupted ZIP.","triggerScenarios":"Calling extractOfficeText (via the office-attachments plugin) on a Buffer whose local header's data region extends past the end of the file: truncated download/upload, byte-sliced buffer, or central-directory sizes disagreeing with a locally re-parsed truncated file.","commonSituations":"Partially uploaded or interrupted transfers of docx/xlsx files stored via the attachments plugin; files corrupted by text-mode transfer; a caller passing a truncated slice of the original buffer.","solutions":["Re-upload or re-download the Office file; verify byte size matches the original.","Check that the caller passes the complete file Buffer, not a truncated slice.","Remove any transfer that could mangle bytes (e.g., text-mode FTP, bad base64 decode) and retest.","If files are intentionally larger than limits, confirm MAX_ENTRY_UNCOMPRESSED_BYTES/limits are not causing upstream truncation."],"exampleFix":"// before: passing a possibly truncated slice\nconst data = readZipEntryData(buffer.subarray(0, 1000), entry);\n// after: pass the full original buffer\nconst data = readZipEntryData(fullBytes, entry);","handlingStrategy":"validation","validationCode":"import { stat } from \"node:fs/promises\";\nasync function zipLooksComplete(filePath: string): Promise<boolean> {\n  const buf = await readFile(filePath);\n  // EOCD signature \"PK\\x05\\x06\" must appear in the final 64KB\n  const eocd = buf.subarray(Math.max(0, buf.byteLength - 65558)).lastIndexOf(Buffer.from(\"PK0506\", \"hex\"));\n  return eocd !== -1;\n}","typeGuard":"function isTruncated(buf: Buffer, declaredEnd: number): boolean {\n  return declaredEnd > buf.byteLength; // true => refuse to parse\n}","tryCatchPattern":"try {\n  const text = extractOfficeText(kind, bytes);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"is out of bounds\")) {\n    throw new ApiError(400, \"corrupt_attachment\", \"Attachment archive is truncated or corrupted; please re-upload.\");\n  }\n  throw err;\n}","preventionTips":["Verify file size/checksum (e.g., sha256) after download before extraction.","Use binary-safe transfers only; avoid text-mode transformations.","Check the ZIP End-of-Central-Directory record exists before parsing.","Reject uploads that fail the platform's own zip integrity check (unzip -t)."],"tags":["zip","corrupt-file","parsing"],"backgroundTag":"zip-entry-out-of-bounds","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}