{"record":{"id":"1c11940eee8171e5","repo":"Hmbown/CodeWhale","slug":"invalid-update-file-header","errorCode":null,"errorMessage":"Invalid update file header.","messagePattern":"Invalid update file header\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/app/updates.mjs","lineNumber":66,"sourceCode":"\n/** Inspect both ZIP headers before extraction: no links, traversal or bombs. */\nexport function validateReleaseZip(bytes) {\n  const minimum=Math.max(0,bytes.length-65557); let end=-1;\n  for(let i=bytes.length-22;i>=minimum;i--) if(bytes.readUInt32LE(i)===0x06054b50&&i+22+bytes.readUInt16LE(i+20)===bytes.length) { end=i; break; }\n  if(end<0||bytes.readUInt16LE(end+4)||bytes.readUInt16LE(end+6)) throw new Error(\"Invalid update archive.\");\n  const count=bytes.readUInt16LE(end+10); let position=bytes.readUInt32LE(end+16),total=0;\n  if(!count||count>2000||bytes.readUInt16LE(end+8)!==count||position+bytes.readUInt32LE(end+12)!==end) throw new Error(\"Invalid update archive index.\");\n  const seen=new Set();\n  for(let i=0;i<count;i++) {\n    if(position+46>end||bytes.readUInt32LE(position)!==0x02014b50) throw new Error(\"Invalid update entry.\");\n    const flags=bytes.readUInt16LE(position+8),method=bytes.readUInt16LE(position+10),length=bytes.readUInt16LE(position+28),extra=bytes.readUInt16LE(position+30),comment=bytes.readUInt16LE(position+32);\n    const name=bytes.subarray(position+46,position+46+length).toString(\"utf8\");\n    const kind=(bytes.readUInt32LE(position+38)>>>16)&0xf000,offset=bytes.readUInt32LE(position+42),compressed=bytes.readUInt32LE(position+20);\n    const size=bytes.readUInt32LE(position+24); total+=size;\n    if(flags&1||![0,8].includes(method)||![0,0x4000,0x8000].includes(kind)||total>512*1024*1024||position+46+length+extra+comment>end) throw new Error(\"Unsupported update entry.\");\n    if(!name.startsWith(`${APP_NAME}.app/`)||name.includes(\"\\\\\")||name.includes(\":\")||name.includes(\"\\0\")||name.split(\"/\").some(part=>part===\"..\"||part===\".\")||seen.has(name)) throw new Error(\"Unsafe update path.\");\n    seen.add(name);\n    if(offset+30>position||bytes.readUInt32LE(offset)!==0x04034b50) throw new Error(\"Invalid update file header.\");\n    const localLength=bytes.readUInt16LE(offset+26),localExtra=bytes.readUInt16LE(offset+28);\n    if(offset+30+localLength+localExtra+compressed>bytes.readUInt32LE(end+16)||bytes.subarray(offset+30,offset+30+localLength).toString(\"utf8\")!==name) throw new Error(\"Inconsistent update file header.\");\n    if(bytes.readUInt16LE(offset+8)!==method||bytes.readUInt16LE(offset+6)!==flags||(!(flags&8)&&(bytes.readUInt32LE(offset+18)!==compressed||bytes.readUInt32LE(offset+22)!==size))) throw new Error(\"Inconsistent update sizes or compression.\");\n    const start=offset+30+localLength+localExtra;\n    // Header sizes are untrusted. Bound actual expansion before ditto writes\n    // anything, including a compressed payload whose headers understate size.\n    const payload=bytes.subarray(start,start+compressed);\n    let expanded;\n    try { expanded=method===0?payload.length:inflateRawSync(payload,{maxOutputLength:Math.max(size,1)}).length; }\n    catch { throw new Error(\"Invalid or oversized compressed update entry.\"); }\n    if(expanded!==size) throw new Error(\"The update entry size did not match its contents.\");\n    position+=46+length+extra+comment;\n  }\n  if(position!==end) throw new Error(\"Invalid update archive length.\");\n  return count;\n}\n\nexport async function prepareUpdate(update) {","sourceCodeStart":48,"sourceCodeEnd":84,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/app/updates.mjs#L48-L84","documentation":"For each central-directory entry the validator follows its stored local-header offset and requires (a) the 30-byte fixed local header fits inside the region already scanned, and (b) the bytes there carry the local file header signature 0x04034b50 (PK\\x03\\x04). If the offset is out of range or the signature is missing, the central directory and local entries disagree — the archive is malformed or hand-forged — and extraction is refused.","triggerScenarios":"A central-directory entry whose `offset` (read at position+42) points beyond the current central-directory position, into the central directory itself, or at bytes that are not a local file header — e.g. data-descriptor-based zips with corrupted offsets, zip-bomb constructions, or archives modified after the central directory was written.","commonSituations":"Appending data to a zip without rewriting the central directory; tools that strip or relocate local headers (some 'zip optimizer' utilities); a deliberately crafted archive probing the parser; truncated file where offsets were written for a longer original.","solutions":["Regenerate the archive with a standard tool (`ditto -c -k` or `zip -r`) instead of post-processing or concatenating zip files.","Verify with `unzip -t file.zip` — it reports mismatched local headers as 'bad zipfile' — before publishing.","Re-download and re-verify the SHA-256 of the release asset; the bytes may be truncated or tampered in transit.","Never edit zip offsets by hand in build scripts; rebuild the artifact from source."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"if (execFileSync(\"unzip\", [\"-t\", zipPath], {stdio:\"pipe\"}).status !== 0) throw new Error(\"Archive fails unzip -t\");","typeGuard":null,"tryCatchPattern":"try { validateReleaseZip(bytes); } catch (e) { if (e.message === \"Invalid update file header.\") throw new Error(\"Central-directory offsets are corrupt — rebuild the archive\"); throw e; }","preventionTips":["Never post-process, prepend stubs to, or hand-edit finished zips","Run `unzip -t` as a packaging gate","Keep one zip writer for the whole artifact","SHA-256-verify downloaded bytes before validation"],"tags":["zip","archive-parsing","corrupt-file","update-integrity"],"backgroundTag":"checksum-mismatch","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}