mastra-ai/mastra · error

${label} escapes workspace

Error message

${label} escapes workspace

What it means

assertRelativePath validates that a query path parameter (labelled e.g. 'path' or 'root') is a safe relative path inside the workspace before it is used for filesystem access. The factory throws "<label> escapes workspace" when the path contains a '..' segment or normalizes outside the root, because such a path could traverse out of the confined workspace directory. It is a deliberate path-traversal guard, not a bug.

Source

Thrown at mastracode/factory/src/routes/fs.ts:218

 * Resolve a path's real location (following symlinks) and confirm it stays
 * within `root`. Returns the real path when confined, or `null` when it escapes
 * the root or does not exist. Used so a symlink inside the root that points
 * outside it cannot be browsed or selected.
 */
async function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {
  try {
    const real = await realpath(candidate);
    return isWithinRoot(real, root) ? real : null;
  } catch {
    return null;
  }
}

function assertRelativePath(path: string, label: string): string {
  const trimmed = path.trim();
  if (!trimmed) throw new Error(`Missing required query param: ${label}`);
  if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);
  if (trimmed.split(/[\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);
  const normalized = resolve('/', trimmed).slice(1);
  if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))
    throw new Error(`${label} escapes workspace`);
  return normalized;
}

function assertApprovedRenderedRoot(renderedRoot: string): string {
  const safeRoot = assertRelativePath(renderedRoot, 'root');
  if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');
  return safeRoot;
}

async function confinedWorkspacePath(
  root: string,
  workspacePath: string,
): Promise<{ resolvedRoot: string; workspace: string }> {
  const resolvedRoot = await realOrResolved(resolveFsRoot(root));
  const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Remove '..' segments from the path before sending; navigate within the workspace using paths relative to its root
  2. Use path.resolve(workspaceRoot, requested) in the caller and verify the result starts with workspaceRoot before passing it as the query param
  3. Pass an absolute workspacePath via the dedicated workspace param (which is confined via confinedWorkspacePath) instead of escaping a relative one
  4. If upward navigation is needed, request the workspace root itself rather than '..'

Example fix

// before
const res = await fetch(`/api/fs?path=${encodeURIComponent('../other-project/file.txt')}`);
// after
const rel = path.relative(workspaceRoot, targetFile);
if (rel.startsWith('..')) throw new Error('target is outside workspace');
const res = await fetch(`/api/fs?path=${encodeURIComponent(rel)}`);
Defensive patterns

Strategy: validation

Validate before calling

import { isAbsolute, resolve, sep } from 'node:path';
function isSafeRelative(p: string): boolean {
  const t = p.trim();
  if (!t || isAbsolute(t)) return false;
  if (t.split(/[\\/]+/).includes('..')) return false;
  const norm = resolve('/', t).slice(1);
  return !!norm && norm !== '..' && !norm.startsWith(`..${sep}`);
}
if (!isSafeRelative(userPath)) throw new Error('path escapes workspace');

Type guard

function isSafeRelativePath(p: unknown): p is string {
  return typeof p === 'string' && isSafeRelative(p);
}

Try / catch

try {
  const data = await fetchFsRoute({ path: userPath });
} catch (err) {
  if (err instanceof Error && err.message.includes('escapes workspace')) {
    // clamp to workspace root or re-derive with path.relative(root, target)
  } else throw err;
}

Prevention

When it happens

Trigger: Calling any fs route (via safeRoot, safeRelativePath, safePath, or safePreviousPath) with a query param containing '..' segments (e.g. ?path=../secrets), a path that normalizes to empty or '..' (e.g. ?path=..), or a path like 'a/../../b' that resolves above the root after resolve('/', trimmed).

Common situations: Client code joining paths with string concatenation instead of resolve(); passing an absolute path stripped incorrectly; encoding '..%2F' that a framework decodes before validation; tests or scripts reusing paths computed for a different workspace depth; symlink-free traversal attempts in UI 'go up one level' handlers.

Understand the failure class

Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.

Related errors


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