{"record":{"id":"93396d4c7fbdbdc0","repo":"mastra-ai/mastra","slug":"unable-to-verify-path-stays-within-workspace-root","errorCode":null,"errorMessage":"Unable to verify path stays within workspace root: ${inputPath}","messagePattern":"Unable to verify path stays within workspace root: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/sdk/src/agents/sandbox-filesystem.ts","lineNumber":212,"sourceCode":"    const result = await this.exec(\n      [\n        `p=${shellQuote(abs)}`,\n        `if [ ! -e \"$p\" ] && [ ! -L \"$p\" ]; then exit ${EXIT_NOT_FOUND}; fi`,\n        // The workdir itself may contain symlinked components (/tmp on macOS),\n        // so canonicalize it as the comparison root.\n        `root=$(cd ${shellQuote(this.basePath)} 2>/dev/null && pwd -P)`,\n        `[ -n \"$root\" ] || exit 1`,\n        `rp=$(realpath \"$p\" 2>/dev/null) || rp=$(readlink -f \"$p\" 2>/dev/null) || { [ -d \"$p\" ] && rp=$(cd \"$p\" 2>/dev/null && pwd -P); }`,\n        `[ -n \"$rp\" ] || exit 1`,\n        `printf '%s\\\\n%s' \"$root\" \"$rp\"`,\n      ].join('\\n'),\n    );\n    // Path doesn't exist yet: nothing to canonicalize (writes to a fresh leaf\n    // are covered by assertContainedDest checking the parent directory).\n    if (result.exitCode === EXIT_NOT_FOUND) return;\n    const [root, real] = result.stdout.split('\\n').map(s => s.trim());\n    if (result.exitCode !== 0 || !root || !real) {\n      throw new Error(`Unable to verify path stays within workspace root: ${inputPath}`);\n    }\n    if (real !== root && !real.startsWith(`${root}/`)) {\n      throw new Error(`Path escapes workspace root (symlink): ${inputPath}`);\n    }\n  }\n\n  /**\n   * Guard for write destinations. The lexical guard catches `..`, but a symlink\n   * inside the workdir can redirect a write outside it. For an existing target\n   * we check its realpath; for a not-yet-existing target we check the realpath\n   * of its nearest existing ancestor directory, since a symlinked parent is the\n   * escape vector (e.g. `link -> /etc` then writing `link/passwd`).\n   */\n  private async assertContainedDest(abs: string, inputPath: string): Promise<void> {\n    // First check the target itself (covers overwriting an existing symlink).\n    await this.assertContainedRealpath(abs, inputPath);\n    // Then check the parent directory's realpath; readlink -f resolves the\n    // nearest existing ancestor when the leaf doesn't exist yet.","sourceCodeStart":194,"sourceCodeEnd":230,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/agents/sandbox-filesystem.ts#L194-L230","documentation":"Before reading, deleting, copying, moving, or removing directories, the sandbox canonicalizes the path (and the workspace root) with a shell `realpath` to detect symlink escapes. If the command fails or returns unparsable output (missing root/real lines), the library cannot prove containment, so it throws rather than silently allowing an unverifiable path. It is a fail-closed safety check.","triggerScenarios":"assertContainedRealpath is invoked (readFile, deleteFile, copyFile, moveFile, rmdir, assertContainedDest) on an existing path whose `realpath` invocation returns a non-zero exit other than the handled not-found code, or whose stdout does not contain both the real workspace root and the resolved path (e.g. `\\n`-split lines missing/empty).","commonSituations":"Restricted sandbox/container environments where `realpath` (coreutils) is unavailable or blocked; a shell wrapper that injects extra output (banners, MOTD, proxies) corrupting stdout; sandbox exec layer returning stderr content on stdout; exotic filesystems or corrupted mounts making stat calls fail.","solutions":["Ensure the sandbox environment provides a working `realpath` binary reachable by the exec layer","Run a simple smoke exec (e.g. `realpath /`) through the same sandbox exec to confirm clean stdout with no wrapper noise","Re-run the operation — transient exec/shell failures can trip this check","If the environment cannot offer realpath, relax or bypass the symlink check consciously and rely solely on the lexical containment guard"],"exampleFix":"// before (in a stripped container)\nawait fs.readFile('data/file.txt'); // throws: unable to verify\n// after: install coreutils or verify exec works\n//   docker run ... coreutils (provides realpath), then\nawait fs.readFile('data/file.txt');","handlingStrategy":"try-catch","validationCode":"// Probe that the sandbox exec layer returns clean output and realpath exists\nconst probe = await sandbox.exec('realpath /');\nif (probe.exitCode !== 0 || probe.stdout.trim() === '') {\n  throw new Error('Sandbox cannot canonicalize paths: realpath unavailable or stdout polluted');\n}","typeGuard":"function isContainmentVerified(res: { exitCode: number; stdout: string }): boolean {\n  if (res.exitCode === 0) {\n    const [root, real] = res.stdout.split('\\n').map(s => s.trim());\n    return Boolean(root && real);\n  }\n  return false;\n}","tryCatchPattern":"try {\n  const content = await fs.readFile(p);\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Unable to verify path stays within workspace root')) {\n    // fail closed: do not fall back to an unverified read\n    // surface an environment problem (missing realpath / noisy shell)\n  }\n  throw err;\n}","preventionTips":["Use sandbox images that include coreutils (realpath) and keep exec output clean of wrappers/banners","Fail closed on verification failures — never bypass containment checks to 'make it work'","Smoke-test the sandbox exec layer after environment or image changes","Treat injected shell profile output (MOTD, echo in rc files) as a bug and remove it"],"tags":["sandbox","filesystem","symlink","verification-failed","shell"],"backgroundTag":"path-containment-verification-failed","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}