{"record":{"id":"ae1f6f8f1b8c989e","repo":"Hmbown/CodeWhale","slug":"invalid-or-oversized-compressed-update-entry","errorCode":null,"errorMessage":"Invalid or oversized compressed update entry.","messagePattern":"Invalid or oversized compressed update entry\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"crates/tui/plugins/computer-use/app/updates.mjs","lineNumber":76,"sourceCode":"    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) {\n  if(!update?.available) throw new Error(\"Check for an available update first.\");\n  if(!newerVersion(update.version,APP_VERSION)||update.url!==`${repository}/releases/download/v${update.version}/Codewhale-Computer-Use-${update.version}-macos-universal.zip`||!Number.isSafeInteger(update.size)||update.size<=0||update.size>limit) throw new Error(\"The update identity is invalid.\");\n  // Only GitHub's fixed release URL and its asset CDN can serve the bytes.\n  let url=update.url, response;\n  for(let redirects=0;redirects<4;redirects++) {\n    response=await fetch(url,{redirect:\"manual\",signal:AbortSignal.timeout(60_000)});\n    if(![301,302,303,307,308].includes(response.status)) break;\n    const next=new URL(response.headers.get(\"location\"),url);\n    if(next.protocol!==\"https:\"||![\"github.com\",\"release-assets.githubusercontent.com\",\"objects.githubusercontent.com\"].includes(next.hostname)) throw new Error(\"The update download redirected to an unexpected host.\");\n    url=next.href;","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/plugins/computer-use/app/updates.mjs#L58-L94","documentation":"The validator actually inflates each entry's compressed payload with `inflateRawSync`, capped at the declared uncompressed size via maxOutputLength. If zlib throws — corrupt deflate stream, or output exceeding the declared cap — the entry is either not a valid deflate stream or a decompression bomb that understates its true expansion, and the update is rejected before anything is written to disk.","triggerScenarios":"An entry with method 8 whose payload is not valid raw-deflate data, or whose inflated size exceeds the declared `size` (maxOutputLength = size), or method 0 where the stored payload length equals 0 making expansion undecidable — common with truncated files, zip-bomb attempts, or wrong method recorded in the headers.","commonSituations":"A zip bomb where headers declare a tiny size but the stream expands hugely; truncated or bit-rotted downloads; archives where a stored (method 0) entry was declared as deflate; fuzzed/corrupted assets on disk.","solutions":["Re-download the release asset and verify its SHA-256 against the digest from checkForUpdate/releaseUpdate before validating.","Run `unzip -t` locally — it will report CRC/inflate errors on the same entries.","Rebuild and re-upload the release artifact; the published bytes themselves are bad.","If you build archives yourself, confirm every entry is deflate (or stored) with sizes matching actual content, e.g. round-trip `zip`/`unzip -t` in CI."],"exampleFix":"// before\nconst bytes = fs.readFileSync(zipPath);\nvalidateReleaseZip(bytes);\n// after\nconst bytes = fs.readFileSync(zipPath);\nconst digest = crypto.createHash(\"sha256\").update(bytes).digest(\"hex\");\nif (digest !== update.sha256) throw new Error(\"Downloaded update is corrupt — retry the download.\");\nvalidateReleaseZip(bytes);","handlingStrategy":"validation","validationCode":"const bytes = fs.readFileSync(zipPath);\nconst digest = crypto.createHash(\"sha256\").update(bytes).digest(\"hex\");\nif (update.sha256 && digest !== update.sha256) throw new Error(\"Corrupt download — retry\");","typeGuard":null,"tryCatchPattern":"try { validateReleaseZip(bytes); } catch (e) { if (e.message === \"Invalid or oversized compressed update entry.\") throw new Error(\"Entry fails to inflate within its declared size — re-download or republish\"); throw e; }","preventionTips":["Always verify SHA-256 of downloaded assets before validating","Run `unzip -t` (CRC check) before publishing","Design packaging so declared sizes equal real content","Retry the download on any validation failure — corrupted transfers are the most common cause"],"tags":["zip","zlib","decompression-bomb","corrupt-file","update-integrity"],"backgroundTag":"payload-too-large","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"}