{"record":{"id":"d940b05826150041","repo":"stablyai/orca","slug":"spritesheetpath-must-be-relative-to-the-bundle","errorCode":null,"errorMessage":"spritesheetPath must be relative to the bundle.","messagePattern":"spritesheetPath must be relative to the bundle\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/pet.ts","lineNumber":290,"sourceCode":"    try {\n      const raw = await readFile(manifestPath, 'utf8')\n      // Why: defend against TOCTOU — the file may have grown between the stat check and this read.\n      if (Buffer.byteLength(raw, 'utf8') > MAX_MANIFEST_BYTES) {\n        throw new Error('pet.json exceeded the manifest size limit.')\n      }\n      manifest = applyCodexPetDefaults(PetManifestSchema.parse(JSON.parse(raw)))\n    } catch (error) {\n      throw new Error(`Invalid pet.json: ${error instanceof Error ? error.message : 'parse error'}`)\n    }\n\n    // Why: spritesheetPath is bundle-relative and attacker-controlled — reject absolute/escaping paths (and symlinks) so a bundle can't reach outside.\n    const normalizedSpritePath = manifest.spritesheetPath.replace(/[\\\\/]+/g, sep)\n    if (\n      isAbsolute(manifest.spritesheetPath) ||\n      isAbsolute(normalizedSpritePath) ||\n      /^[a-zA-Z]:/.test(manifest.spritesheetPath)\n    ) {\n      throw new Error('spritesheetPath must be relative to the bundle.')\n    }\n    // Why: bundles exported on Windows may be imported on macOS/Linux; normalize separators before resolving.\n    const sheetSrc = resolve(bundleDir, normalizedSpritePath)\n    const bundleResolved = resolve(bundleDir)\n    if (sheetSrc === bundleResolved) {\n      throw new Error('spritesheetPath must point to a file, not the bundle root.')\n    }\n    const bundleRoot = bundleResolved + sep\n    // Why: Windows volumes are case-insensitive; lowercase the prefix compare so case differences can't bypass the escape check.\n    const cmp = process.platform === 'win32' ? (s: string) => s.toLowerCase() : (s: string) => s\n    if (!cmp(sheetSrc + sep).startsWith(cmp(bundleRoot))) {\n      throw new Error('spritesheetPath escapes the bundle.')\n    }\n    if (await isSymlink(sheetSrc)) {\n      throw new Error('spritesheet must not be a symlink.')\n    }\n    const sheetClass = classifyFile(sheetSrc)\n    if (!sheetClass || sheetClass.ext === '.svg') {","sourceCodeStart":272,"sourceCodeEnd":308,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/pet.ts#L272-L308","documentation":"Thrown at pet.ts:284-290 when the manifest's spritesheetPath is absolute on POSIX, absolute after backslash-to-sep normalization, or matches a Windows drive-letter prefix (e.g. C:\\). This is a hard security gate: spritesheetPath is attacker-controlled (it comes from the bundle's pet.json) and is resolved relative to the bundle directory, so an absolute value would let a bundle read an arbitrary file outside it.","triggerScenarios":"pet.json sets spritesheetPath to '/etc/passwd', 'C:\\Windows\\secret.png', '\\\\?\\C:\\x.png', or any value that isAbsolute() flags (after replacing backslash runs with sep). The check at lines 285-289 runs immediately after PetManifestSchema.parse succeeds.","commonSituations":"A bundle author hard-codes an absolute path from their own machine; a Windows-exported bundle carries a drive-letter path; a malicious bundle crafted to exfiltrate a known file.","solutions":["Set spritesheetPath to a path relative to the bundle directory, e.g. \"spritesheet.png\" or \"assets/cat.png\".","Remove any leading '/', '\\\\', or drive-letter prefix from the value in pet.json.","Re-export the bundle so the path is relative, then re-import."],"exampleFix":"// before (pet.json)\n{ \"spritesheetPath\": \"/home/user/cat.png\" }\n// after\n{ \"spritesheetPath\": \"cat.png\" }","handlingStrategy":"validation","validationCode":"import { isAbsolute } from 'node:path'\nfunction assertRelativeSheetPath(p: string | undefined) {\n  if (p === undefined) return\n  const normalized = p.replace(/[\\\\/]+/g, '/')\n  if (isAbsolute(p) || isAbsolute(normalized) || /^[a-zA-Z]:/.test(p)) {\n    throw new Error(`spritesheetPath must be relative: ${p}`)\n  }\n}","typeGuard":"function isRelativeBundlePath(p: string): boolean {\n  return !p.includes('\\0') && !p.startsWith('/') && !p.startsWith('\\\\') && !/^[a-zA-Z]:/.test(p)\n}","tryCatchPattern":"try { await importPetBundle(p) }\ncatch (e) { if (e instanceof Error && e.message === 'spritesheetPath must be relative to the bundle.') { /* prompt user to fix manifest */ } else throw e }","preventionTips":["Always emit spritesheetPath as a bare relative filename from your bundle generator.","Never hardcode absolute paths from your authoring machine into a portable bundle."],"tags":["pet-bundle","security","path-traversal","validation"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}