mastra-ai/mastra · critical
Path escapes workspace root (symlink): ${inputPath}
Error message
Path escapes workspace root (symlink): ${inputPath} What it means
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.
Source
Thrown at mastracode/sdk/src/agents/sandbox-filesystem.ts:215
`if [ ! -e "$p" ] && [ ! -L "$p" ]; then exit ${EXIT_NOT_FOUND}; fi`,
// The workdir itself may contain symlinked components (/tmp on macOS),
// so canonicalize it as the comparison root.
`root=$(cd ${shellQuote(this.basePath)} 2>/dev/null && pwd -P)`,
`[ -n "$root" ] || exit 1`,
`rp=$(realpath "$p" 2>/dev/null) || rp=$(readlink -f "$p" 2>/dev/null) || { [ -d "$p" ] && rp=$(cd "$p" 2>/dev/null && pwd -P); }`,
`[ -n "$rp" ] || exit 1`,
`printf '%s\\n%s' "$root" "$rp"`,
].join('\n'),
);
// Path doesn't exist yet: nothing to canonicalize (writes to a fresh leaf
// are covered by assertContainedDest checking the parent directory).
if (result.exitCode === EXIT_NOT_FOUND) return;
const [root, real] = result.stdout.split('\n').map(s => s.trim());
if (result.exitCode !== 0 || !root || !real) {
throw new Error(`Unable to verify path stays within workspace root: ${inputPath}`);
}
if (real !== root && !real.startsWith(`${root}/`)) {
throw new Error(`Path escapes workspace root (symlink): ${inputPath}`);
}
}
/**
* Guard for write destinations. The lexical guard catches `..`, but a symlink
* inside the workdir can redirect a write outside it. For an existing target
* we check its realpath; for a not-yet-existing target we check the realpath
* of its nearest existing ancestor directory, since a symlinked parent is the
* escape vector (e.g. `link -> /etc` then writing `link/passwd`).
*/
private async assertContainedDest(abs: string, inputPath: string): Promise<void> {
// First check the target itself (covers overwriting an existing symlink).
await this.assertContainedRealpath(abs, inputPath);
// Then check the parent directory's realpath; readlink -f resolves the
// nearest existing ancestor when the leaf doesn't exist yet.
const parent = posixPath.dirname(abs);
if (parent && parent !== abs) {
await this.assertContainedRealpath(parent, inputPath);View on GitHub (pinned to 75dd419e61)
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)
Example fix
// before
ln -s /etc workdir/etc
await fs.readFile('etc/passwd'); // throws: symlink escape
// after
cp -r /etc/needed-config workdir/config/
await fs.readFile('config/needed-config'); Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs';
export function containsNoEscapingSymlinks(root: string, rel: string): boolean {
const abs = fs.realpathSync(path.join(fs.realpathSync(root), rel));
const realRoot = fs.realpathSync(root);
return abs === realRoot || abs.startsWith(realRoot + path.sep);
}
if (!containsNoEscapingSymlinks(WORKSPACE_ROOT, target)) {
throw new Error('Refusing: target resolves outside the workspace via symlink');
} Type guard
function isRealpathInsideRoot(root: string, real: string): boolean {
return real === root || real.startsWith(`${root}/`);
} Try / catch
try {
await fs.readFile(p);
} catch (err) {
if (err instanceof Error && err.message.includes('Path escapes workspace root (symlink)')) {
// quarantine the path; inspect and remove the offending symlink
// never retry unchanged — retrying keeps following the escape
}
throw err;
} Prevention
- 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
When it happens
Trigger: 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`.
Common situations: 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.
Related errors
- Path is outside the browsable root
- Path is outside the workspace
- Refusing to use local sandbox path outside configured root:
- Path escapes workspace root: ${inputPath}
- Invalid route path: "${path}". Path cannot contain '..', '?'
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/6a169ae54529a009.
Report an issue: GitHub.