{"record":{"id":"1812faac9359c886","repo":"stablyai/orca","slug":"pet-json-must-not-be-a-symlink","errorCode":null,"errorMessage":"pet.json must not be a symlink.","messagePattern":"pet\\.json must not be a symlink\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ipc/pet.ts","lineNumber":268,"sourceCode":"    try {\n      const pickedStat = await stat(picked)\n      bundleDir = pickedStat.isDirectory() ? picked : dirname(picked)\n    } catch {\n      throw new Error('Could not read the selected path.')\n    }\n\n    const manifestPath = join(bundleDir, 'pet.json')\n    let manifestStat: Awaited<ReturnType<typeof stat>>\n    try {\n      manifestStat = await stat(manifestPath)\n    } catch {\n      throw new Error('Bundle is missing pet.json.')\n    }\n    if (!manifestStat.isFile() || manifestStat.size > MAX_MANIFEST_BYTES) {\n      throw new Error('pet.json is invalid.')\n    }\n    if (await isSymlink(manifestPath)) {\n      throw new Error('pet.json must not be a symlink.')\n    }\n\n    let manifest: ResolvedPetManifest<PetManifest>\n    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) ||","sourceCodeStart":250,"sourceCodeEnd":286,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/ipc/pet.ts#L250-L286","documentation":"Thrown by the pet:importPetBundle handler when isSymlink(manifestPath) returns true — lstat detected that pet.json is a symbolic link rather than a regular file. The check is a security guard: a symlinked manifest could point anywhere on the filesystem (or to a network path), enabling a malicious bundle to substitute content after the validation passes or to reference files outside the bundle.","triggerScenarios":"pet.json is a symlink (created via ln -s on macOS/Linux or mklink on Windows) pointing to another location, or a bundle-extraction tool preserved symlinks instead of materializing the target file.","commonSituations":"User authored the bundle using symlinks for convenience, an archive extractor preserved symlinks, or a malicious bundle intentionally symlinked pet.json to an external target.","solutions":["Replace the symlink with a real, regular pet.json file (cp the target content into place, remove the symlink).","Re-extract the bundle with an option that materializes symlinks as regular files.","On macOS/Linux run 'ls -l pet.json' — a leading 'l' in the mode confirms a symlink.","Never ship pet.json as a symlink in a redistributable bundle."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"import { lstat } from 'node:fs/promises'\nimport { join } from 'node:path'\n\nasync function manifestIsSymlink(bundleDir: string): Promise<boolean> {\n  try {\n    return (await lstat(join(bundleDir, 'pet.json'))).isSymbolicLink()\n  } catch {\n    return false\n  }\n}\n\nif (await manifestIsSymlink(bundleDir)) {\n  notify('pet.json must be a real file, not a symlink.')\n  return\n}","typeGuard":null,"tryCatchPattern":"try {\n  await ipcRenderer.invoke('pet:importPetBundle')\n} catch (e) {\n  if (e instanceof Error && e.message === 'pet.json must not be a symlink.') {\n    notify('Replace the pet.json symlink with a real file and re-import.')\n  } else throw e\n}","preventionTips":["Ship pet.json as a regular file in redistributable bundles.","Extract bundles with an option that materializes symlinks as files.","On macOS/Linux, check `ls -l pet.json` for a leading 'l'."],"tags":["ipc","pet-overlay","filesystem","security","symlink","bundle-import","manifest"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}