{"record":{"id":"c88c2b3b1ebc1fe2","repo":"JuliusBrussee/caveman","slug":"cave-tool-sandbox-entry-escapes-root","errorCode":"cave_tool_sandbox_entry_escapes_root","errorMessage":"cave_tool_sandbox_entry_escapes_root","messagePattern":"cave_tool_sandbox_entry_escapes_root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":2270,"sourceCode":"    if (activeExecution) await activeExecution.catch(() => undefined);\n    if (activeAbort) options.signal?.removeEventListener(\"abort\", activeAbort);\n    releaseConversation();\n    // Idempotent backstop for the abort/return() paths that never reached a\n    // terminal yield; the success and error paths already released above.\n    await releaseRunResources();\n  }\n}\n\nasync function stageSandboxSourceGraph(\n  root: string,\n  entryPath: string,\n  frameworkDistRoot: string,\n): Promise<SandboxSourceSnapshot> {\n  const canonicalRoot = await realpath(root);\n  const canonicalEntry = await realpath(entryPath);\n  const entryName = relative(canonicalRoot, canonicalEntry);\n  if (escapesRoot(entryName)) {\n    throw new Error(\"cave_tool_sandbox_entry_escapes_root\");\n  }\n  const canonicalFrameworkDist = await realpath(frameworkDistRoot);\n  const graph = [...await expandSourceGraph(\n    canonicalRoot,\n    [canonicalEntry],\n    false,\n    [canonicalFrameworkDist],\n  )].sort();\n  const staging = await realpath(await mkdtemp(resolve(tmpdir(), \"caveman-agent-source-\")));\n  try {\n    const stagedFiles: string[] = [];\n    await copyOptionalSandboxFile(\n      resolve(canonicalRoot, \"package.json\"),\n      resolve(staging, \"package.json\"),\n      stagedFiles,\n    );\n    for (const source of graph) {\n      if (!escapesRoot(relative(canonicalFrameworkDist, source))) {","sourceCodeStart":2252,"sourceCodeEnd":2288,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L2252-L2288","documentation":"When building a sandbox source snapshot, the library canonicalizes (realpath) the sandbox root and the requested entry file and computes the entry's path relative to the root. If that relative path escapes the root (e.g. contains '..' segments or is absolute), it throws this error to block reading files outside the sandbox. This is a security guard against symlink- or path-based escape from the sandbox root.","triggerScenarios":"Calling the sandbox snapshot function with an entryPath that resolves (after realpath/symlink resolution) outside the canonical sandbox root — e.g. an absolute path elsewhere on disk, '../' traversal, or a symlink inside root pointing to an external file.","commonSituations":"Passing a user-supplied file path directly as the entry without validating it's inside the sandbox; symlinks created at runtime that point out of the staging dir; constructing entry paths by string concatenation with untrusted input.","solutions":["Validate that the entry path resolves inside the sandbox root before calling: compare path.relative(realpath(root), realpath(entry)) and reject values starting with '..' or absolute.","Copy/symlink the entry file into the sandbox root rather than referencing it externally.","Sanitize/reject user input containing '..' or absolute paths before it reaches the entry parameter.","Remove or replace out-of-root symlinks inside the sandbox root that redirect the entry elsewhere."],"exampleFix":"// before\nawait buildSandboxSnapshot(root, userInputPath, frameworkDistRoot); // may be anywhere\n// after\nconst realEntry = await realpath(userInputPath);\nconst rel = relative(await realpath(root), realEntry);\nif (rel.startsWith('..') || isAbsolute(rel)) throw new Error('entry must live inside sandbox root');\nawait buildSandboxSnapshot(root, realEntry, frameworkDistRoot);","handlingStrategy":"validation","validationCode":"import { realpath, relative, isAbsolute } from 'path';\nasync function entryIsInsideRoot(root: string, entry: string): Promise<boolean> {\n  const [cr, ce] = await Promise.all([realpath(root), realpath(entry)]);\n  const rel = relative(cr, ce);\n  return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);\n}\n// call before snapshotting: if (!(await entryIsInsideRoot(root, entryPath))) reject;","typeGuard":null,"tryCatchPattern":"try {\n  const snapshot = await buildSandboxSnapshot(root, entryPath, frameworkDistRoot);\n} catch (err) {\n  if (err instanceof Error && err.message === 'cave_tool_sandbox_entry_escapes_root') {\n    // reject the request / copy the entry into the sandbox root and retry\n  } else throw err;\n}","preventionTips":["Never pass raw user input as the entry path; resolve and contain it under the sandbox root first.","Treat symlinks as hostile: realpath the entry (and everything under root) before trusting it.","Add tests covering '../' traversal and absolute paths in the entry parameter.","Keep the sandbox root as the sole mount point for user-accessible files."],"tags":["security","sandbox","path-traversal","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}