{"record":{"id":"6a169ae54529a009","repo":"mastra-ai/mastra","slug":"path-escapes-workspace-root-symlink-inputpath","errorCode":null,"errorMessage":"Path escapes workspace root (symlink): ${inputPath}","messagePattern":"Path escapes workspace root \\(symlink\\): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"mastracode/sdk/src/agents/sandbox-filesystem.ts","lineNumber":215,"sourceCode":"        `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.\n    const parent = posixPath.dirname(abs);\n    if (parent && parent !== abs) {\n      await this.assertContainedRealpath(parent, inputPath);","sourceCodeStart":197,"sourceCodeEnd":233,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/sdk/src/agents/sandbox-filesystem.ts#L197-L233","documentation":"The sandbox checks not only lexical `..` escapes but also filesystem-level escapes via symlinks: it canonicalizes the target with `realpath` and requires the real path to remain under the real workspace root. If a symlink inside the workdir points outside it, writes/reads through that link would bypass the sandbox, so the operation is refused. This is a critical security guard against symlink-based sandbox escape.","triggerScenarios":"readFile, deleteFile, copyFile, moveFile, rmdir, or a guarded write destination (assertContainedDest) is given a path that exists but is (or passes through) a symlink resolving outside the workspace root — e.g. the workdir contains `link -> /etc` and the caller reads `link/passwd`.","commonSituations":"Extracting user/archived content (tarballs, uploads, git repos) into the workspace that contains pre-existing symlinks; an agent creating a symlink to a host directory and then following it; shared volumes with dangling or absolute symlinks; tests that fixture symlinks pointing outside tmp workdirs.","solutions":["Remove or rewrite the offending symlink so it targets a path inside the workspace root","Copy the real target into the workspace and reference the copy instead of linking out","Reconfigure the sandbox root to legitimately include the symlink destination if that access is intended","Sanitize any externally-supplied archive/content before extracting it into the workspace (reject absolute or escaping symlink entries)"],"exampleFix":"// before\nln -s /etc workdir/etc\nawait fs.readFile('etc/passwd'); // throws: symlink escape\n// after\ncp -r /etc/needed-config workdir/config/\nawait fs.readFile('config/needed-config');","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nexport function containsNoEscapingSymlinks(root: string, rel: string): boolean {\n  const abs = fs.realpathSync(path.join(fs.realpathSync(root), rel));\n  const realRoot = fs.realpathSync(root);\n  return abs === realRoot || abs.startsWith(realRoot + path.sep);\n}\nif (!containsNoEscapingSymlinks(WORKSPACE_ROOT, target)) {\n  throw new Error('Refusing: target resolves outside the workspace via symlink');\n}","typeGuard":"function isRealpathInsideRoot(root: string, real: string): boolean {\n  return real === root || real.startsWith(`${root}/`);\n}","tryCatchPattern":"try {\n  await fs.readFile(p);\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Path escapes workspace root (symlink)')) {\n    // quarantine the path; inspect and remove the offending symlink\n    // never retry unchanged — retrying keeps following the escape\n  }\n  throw err;\n}","preventionTips":["Reject absolute or `..`-targeting symlink entries when extracting archives/uploads into the workspace","Periodically scan the workspace for symlinks whose targets resolve outside the root","Seed workspaces from trusted content only; re-sanitize after external tools or agents write into the workdir","Prefer copies over symlinks when importing external material into the sandbox"],"tags":["sandbox","symlink","path-traversal","security"],"backgroundTag":"symlink-sandbox-escape","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}