{"record":{"id":"f175ad8bfa5f0886","repo":"n8n-io/n8n","slug":"label-candidate-escapes-root","errorCode":null,"errorMessage":"${label} \"${candidate}\" escapes ${root}","messagePattern":"(.+?) \"(.+?)\" escapes (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/@n8n/instance-ai/evaluations/computer-use/runner.ts","lineNumber":186,"sourceCode":"\nfunction resolveFixture(fixturesDir: string, fixturePath: string): string {\n\treturn resolveInside(fixturesDir, fixturePath, 'fixture path');\n}\n\n/**\n * Join `candidate` onto `root` and assert the result stays within `root`.\n * Throws if the resolved path escapes (e.g. via `..`). Used to keep scenario\n * authors honest when declaring fixture paths and sandbox destinations.\n *\n * Exported for unit testing — keep the import surface narrow.\n */\nexport function resolveInside(root: string, candidate: string, label: string): string {\n\tconst rootResolved = resolve(root);\n\tconst fullResolved = resolve(rootResolved, candidate);\n\t// Allow the root itself (e.g. empty candidate) as a no-op destination;\n\t// otherwise require strict containment.\n\tif (fullResolved !== rootResolved && !isContained(rootResolved, fullResolved)) {\n\t\tthrow new Error(`${label} \"${candidate}\" escapes ${root}`);\n\t}\n\treturn fullResolved;\n}\n\n// ---------------------------------------------------------------------------\n// Optional pre-seeded workflow (for scenarios that say \"look at my workflow X\")\n// ---------------------------------------------------------------------------\n\nasync function maybeSeedWorkflow(\n\tclient: N8nClient,\n\tscenario: Scenario,\n\tfixturesDir: string,\n\tlogger: EvalLogger,\n): Promise<void> {\n\tconst path = scenario.setup?.seedWorkflow;\n\tif (!path) return;\n\n\tconst fixturePath = resolveFixture(fixturesDir, path);","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/instance-ai/evaluations/computer-use/runner.ts#L168-L204","documentation":"Thrown by `resolveInside()` in computer-use/runner.ts when joining a scenario-declared path (fixture path or sandbox destination) onto a root directory would produce a resolved path outside that root. The check uses `node:path.resolve` then `isContained(root, full)`; the root itself is allowed (empty candidate is a no-op), but any `..` segment or absolute path that escapes the root is rejected. Purpose: keep scenario JSON authors honest so a malicious or careless fixture path can't read or write outside the sandbox.","triggerScenarios":"A scenario JSON declares `seedWorkflow: '../../../etc/passwd'` or a fixture path like `'/etc/secrets.json'` (absolute, escapes). A sandbox `to: '../../../../tmp'` declared in a seed entry. Any fixture/destination resolved relative to `fixturesDir` or `sandboxDir` that does not stay underneath it. Called from `resolveFixture` (label 'fixture path') and from the seed loop (label 'sandbox path'), so the `label` in the message tells you which.","commonSituations":"Scenario author uses an absolute path by mistake. A copy-pasted fixture path from a different scenario layout where the relative root differs. A legitimate intent to share a fixture from outside the fixtures dir — the model forbids this; you must put the fixture inside the fixtures dir or symlink/copy it in.","solutions":["Open the scenario JSON cited by the run and find the field matching the `label` in the message ('fixture path' → check `seedWorkflow`/fixture refs; 'sandbox path' → check `setup.seeds[].to`).","Replace the escaping path with one that resolves strictly under the root: use a relative path with no `..`, or copy the external file into the fixtures dir first.","If you genuinely need a path outside the root, that's unsupported by design — restructure so the file lives under `fixtures/` or `sandboxDir`."],"exampleFix":"// before (scenario.json)\n{ \"setup\": { \"seeds\": [{ \"from\": \"data.json\", \"to\": \"../../shared/out.json\" }] } }\n// after\n{ \"setup\": { \"seeds\": [{ \"from\": \"data.json\", \"to\": \"shared/out.json\" }] } }","handlingStrategy":"type-guard","validationCode":"import { resolve, relative } from 'node:path';\nfunction isContained(root: string, child: string): boolean {\n  const rel = relative(root, child);\n  return rel === '' || (!rel.startsWith('..') && !resolve(child).isAbsolute);\n}\nfunction safeResolveInside(root: string, candidate: string): string | null {\n  const r = resolve(root);\n  const f = resolve(r, candidate);\n  return f === r || isContained(r, f) ? f : null;\n}","typeGuard":"function isSafePath(root: string, candidate: string): boolean {\n  const r = resolve(root);\n  const f = resolve(r, candidate);\n  if (f === r) return true;\n  const rel = relative(r, f);\n  return rel.length > 0 && !rel.startsWith('..') && !path.isAbsolute(rel);\n}","tryCatchPattern":null,"preventionTips":["When authoring scenario JSON, never use absolute paths or `..` in fixture/seed destinations — keep everything relative to the declared root.","Validate scenario files against the case-file schema (which can encode path constraints) before committing.","If you need to share fixtures across scenarios, copy them into the fixtures dir rather than reaching out."],"tags":["path-traversal","security","sandbox","scenario-config","validation"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}