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

  1. Remove or rewrite the offending symlink so it targets a path inside the workspace root
  2. Copy the real target into the workspace and reference the copy instead of linking out
  3. Reconfigure the sandbox root to legitimately include the symlink destination if that access is intended
  4. 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

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


AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30). Data as JSON: /api/errors/6a169ae54529a009. Report an issue: GitHub.