{"record":{"id":"5eea8ee6a8039361","repo":"JuliusBrussee/caveman","slug":"cave-live-eval-sandbox-profile-escapes-root","errorCode":"cave_live_eval_sandbox_profile_escapes_root","errorMessage":"cave_live_eval_sandbox_profile_escapes_root","messagePattern":"cave_live_eval_sandbox_profile_escapes_root","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/agent/src/cli.ts","lineNumber":1240,"sourceCode":"    throw new Error(\"cave_fixture_terminal_evidence_missing\", { cause: error });\n  }\n}\n\nasync function loadEvalSandboxProfile(\n  root: string,\n  fixture: EvalDefinition,\n): Promise<{\n  network: boolean;\n  childProcess: boolean;\n  credentialEnv: readonly string[];\n}> {\n  const path = fixture.tools.sandbox;\n  if (!path) throw new Error(\"cave_live_eval_sandbox_profile_missing\");\n  const fullPath = resolve(root, path);\n  const relativePath = relative(root, fullPath);\n  if (relativePath === \"..\" || relativePath.startsWith(\"../\") ||\n      relativePath.startsWith(\"..\\\\\") || isAbsolute(relativePath)) {\n    throw new Error(\"cave_live_eval_sandbox_profile_escapes_root\");\n  }\n  const parsed = JSON.parse(await readFile(fullPath, \"utf8\")) as Record<string, unknown>;\n  const keys = Object.keys(parsed).sort();\n  const expected = [\"child_process\", \"credential_env\", \"network\", \"schema_version\"];\n  if (keys.length !== expected.length || keys.some((key, index) => key !== expected[index]) ||\n      parsed.schema_version !== 1 || typeof parsed.network !== \"boolean\" ||\n      typeof parsed.child_process !== \"boolean\" || !Array.isArray(parsed.credential_env) ||\n      parsed.credential_env.some((name) => typeof name !== \"string\" ||\n        !/^[A-Z][A-Z0-9_]{0,127}$/.test(name))) {\n    throw new Error(\"cave_live_eval_sandbox_profile_invalid\");\n  }\n  validateSandboxCredentialEnv(parsed.credential_env as string[]);\n  return {\n    network: parsed.network,\n    childProcess: parsed.child_process,\n    credentialEnv: parsed.credential_env as string[],\n  };\n}","sourceCodeStart":1222,"sourceCodeEnd":1258,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/agent/src/cli.ts#L1222-L1258","documentation":"loadEvalSandboxProfile() resolves the fixture's sandbox profile path against the project root and rejects any profile that resolves outside the root: if the root-relative path is '..', starts with '../' (or '..\\\\') or is absolute, it throws 'cave_live_eval_sandbox_profile_escapes_root'. This path-containment check guarantees live eval sandbox profiles live inside the project, preventing profiles (and the credentials they reference) from being loaded from arbitrary locations.","triggerScenarios":"Declaring fixture.tools.sandbox as a path containing '..' segments (e.g. '../shared/profile.json' or '../../etc/profile.json'), an absolute filesystem path, or any path that escapes the project root, while running a live-mode fixture.","commonSituations":"Sharing one sandbox profile across projects via a parent-directory relative path; passing an absolute path like '/etc/caveman/profile.json'; symlinks/..-heavy paths constructed from user input in generated fixtures.","solutions":["Move the sandbox profile into the project (e.g. eval/sandbox.profile.json) and point fixture.tools.sandbox at the project-relative path","Copy the shared profile into this project instead of referencing it via ../","Reject or sanitize any programmatically supplied sandbox path that contains .. or is absolute"],"exampleFix":"// before\n{ tools: { mode: \"live\", sandbox: \"../shared/sandbox.profile.json\" } }\n// after\n{ tools: { mode: \"live\", sandbox: \"eval/sandbox.profile.json\" } }","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from \"node:path\";\nfunction sandboxPathIsContained(root: string, sandbox: string): boolean {\n  const rel = relative(root, resolve(root, sandbox));\n  return !(rel === \"..\" || rel.startsWith(\"../\") || rel.startsWith(\"..\\\\\") || isAbsolute(rel));\n}\nfor (const f of evals.filter((x) => x.tools.mode === \"live\")) {\n  if (!sandboxPathIsContained(root, f.tools.sandbox!)) {\n    throw new Error(`fixture '${f.id}' sandbox path escapes project root`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  await loadEvalSandboxProfile(root, fixture);\n} catch (error) {\n  if ((error as Error).message === \"cave_live_eval_sandbox_profile_escapes_root\") {\n    throw new Error(`fixture '${fixture.id}' sandbox profile must be inside the project root; copy the profile into the repo and use a relative path`);\n  }\n  throw error;\n}","preventionTips":["Keep all sandbox profiles under the project (e.g. eval/ or config/)","Never accept user-supplied absolute or ..-containing sandbox paths in generated fixtures","Resolve and assert containment of configured paths in CI","Remember containment is on the resolved path — symlinks count as their target location"],"tags":["eval","sandbox","path-traversal","security"],"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"}