{"record":{"id":"24a403c0a52f9647","repo":"JuliusBrussee/caveman","slug":"cave-tool-sandbox-source-escapes-root","errorCode":"cave_tool_sandbox_source_escapes_root","errorMessage":"cave_tool_sandbox_source_escapes_root","messagePattern":"cave_tool_sandbox_source_escapes_root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/runtime.ts","lineNumber":2292,"sourceCode":"    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))) {\n        continue;\n      }\n      const name = relative(canonicalRoot, source);\n      if (escapesRoot(name)) throw new Error(\"cave_tool_sandbox_source_escapes_root\");\n      const destination = resolve(staging, name);\n      await mkdir(dirname(destination), { recursive: true });\n      await copyFile(source, destination);\n      stagedFiles.push(destination);\n    }\n    const frameworkName = relative(canonicalRoot, canonicalFrameworkDist);\n    if (!escapesRoot(frameworkName)) {\n      const frameworkDestination = resolve(staging, frameworkName);\n      await mkdir(dirname(frameworkDestination), { recursive: true });\n      await symlink(\n        canonicalFrameworkDist,\n        frameworkDestination,\n        process.platform === \"win32\" ? \"junction\" : \"dir\",\n      );\n      stagedFiles.push(frameworkDestination);\n    }\n    const nodeModules = await realpath(resolve(canonicalRoot, \"node_modules\"));\n    await symlink(","sourceCodeStart":2274,"sourceCodeEnd":2310,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/runtime.ts#L2274-L2310","documentation":"While staging sandbox sources, files discovered by expandSourceGraph are checked twice: first against the framework dist root (files under the framework dist are copied from there), then against the canonical sandbox root. This error is thrown when a graph source is neither within the framework dist nor within the sandbox root — i.e. a dependency graph expanded to a file outside every allowed root, so copying it would leak content from outside the sandbox.","triggerScenarios":"expandSourceGraph follows imports/resolution (node_modules links, symlinks, tsconfig paths, monorepo cross-package imports) to a file whose path relative to both canonicalFrameworkDist and canonicalRoot escapes — e.g. a symlinked dependency resolving to a global store or a package outside the project root.","commonSituations":"pnpm/yarn PnP or npm symlinked node_modules resolving to a shared store outside the repo; monorepo imports referencing sibling packages outside the designated root; symlinked framework dist or hoisted deps pointing elsewhere on disk.","solutions":["Ensure the sandbox root encompasses all dependency sources: set root to the repo/monorepo root so resolved imports stay inside it.","Install dependencies normally inside the sandbox root (avoid symlinked global stores) or vendor/copy them under the root before snapshotting.","Check for symlinks in node_modules or the framework dist that resolve outside the root and replace them with real copies.","If a dependency legitimately lives outside, add/expand the allowed roots (e.g. pass the correct frameworkDistRoot or bundle the dependency locally)."],"exampleFix":"// before\nawait snapshotSandboxSources('./packages/app/dist', entry, frameworkDist); // imports resolve to sibling packages outside\n// after\nawait snapshotSandboxSources('/repo/root', entry, '/repo/root/node_modules/framework/dist'); // root covers all sources","handlingStrategy":"validation","validationCode":"import { realpathSync, relativeSync } from 'path';\nfunction sourceLiesInAllowedRoots(root: string, frameworkDist: string, source: string): boolean {\n  const cr = realpathSync(root), cf = realpathSync(frameworkDist), cs = realpathSync(source);\n  const inRoot = !relativeSync(cr, cs).startsWith('..');\n  const inDist = !relativeSync(cf, cs).startsWith('..');\n  return inRoot || inDist;\n}\n// pre-scan the dependency graph and fail fast on any source outside both roots","typeGuard":null,"tryCatchPattern":"try {\n  const snapshot = await buildSandboxSnapshot(root, entry, frameworkDistRoot);\n} catch (err) {\n  if (err instanceof Error && err.message === 'cave_tool_sandbox_source_escapes_root') {\n    // widen root / vendor the escaping dependency, then retry\n  } else throw err;\n}","preventionTips":["Use real (copied) node_modules inside the sandbox rather than symlinked package-manager stores.","Set the sandbox root at the monorepo/repo level so cross-package imports stay inside it.","Audit symlinks in node_modules and the framework dist before snapshotting (find -type l).","Keep dependency resolution (tsconfig paths, bundler aliases) pointed inside the sandbox root."],"tags":["security","sandbox","path-traversal","dependencies"],"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"}