mastra-ai/mastra · error

Path escapes workspace

Error message

Path escapes workspace

What it means

confinedWorkspaceRelativePath combines both guards: it sanitizes the relative path with assertRelativePath, confines the workspace via confinedWorkspacePath, then checks that resolve(workspace, safeRelativePath) is still inside the workspace with isWithinRoot before following symlinks. It throws "Path escapes workspace" when the joined candidate resolves outside the confirmed workspace directory (possible when the workspace itself is nested deeper than the root).

Source

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

  root: string,
  workspacePath: string,
): Promise<{ resolvedRoot: string; workspace: string }> {
  const resolvedRoot = await realOrResolved(resolveFsRoot(root));
  const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);
  const workspace = await realPathWithinRoot(candidate, resolvedRoot);
  if (!workspace) throw new Error('Path is outside the browsable root');
  return { resolvedRoot, workspace };
}

async function confinedWorkspaceRelativePath(
  root: string,
  workspacePath: string,
  relativePath: string,
): Promise<{ workspace: string; path: string; relativePath: string }> {
  const safeRelativePath = assertRelativePath(relativePath, 'path');
  const { workspace } = await confinedWorkspacePath(root, workspacePath);
  const candidate = resolve(workspace, safeRelativePath);
  if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');
  const confinedPath = await realPathWithinRoot(candidate, workspace);
  if (!confinedPath) throw new Error('Path is outside the workspace');
  return { workspace, path: confinedPath, relativePath: safeRelativePath };
}

/**
 * List the directories inside `requestedPath`, confined to `root`. An absent or
 * out-of-root path is clamped to the root, so the worst a malicious client can
 * do is browse within the allowed root.
 */
export async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {
  // Resolve the root through symlinks so all confinement checks compare real
  // paths; a symlink that escapes the root is then reliably detectable.
  const resolvedRoot = await realOrResolved(resolveFsRoot(root));

  let target = resolvedRoot;
  if (requestedPath && requestedPath.trim()) {
    const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);

View on GitHub (pinned to 75dd419e61)

Solutions

  1. Compute the relative path against the actual workspace directory (the realpath), not the configured root, and ensure it never begins with '..'
  2. Navigate to sibling directories by requesting their workspace path directly instead of using '..'
  3. Use path.relative(realWorkspace, target) and reject results starting with '..' before the request
  4. If a deeper common root is needed, reconfigure the workspace root so both directories fall under it

Example fix

// before
const rel = '../shared/config.json';
// after
const rel = path.relative(realWorkspaceDir, targetFile);
if (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error('target outside workspace');
const res = await fetch(`/api/fs/read?workspacePath=${ws}&path=${encodeURIComponent(rel)}`);
Defensive patterns

Strategy: validation

Validate before calling

import { resolve, relative, isAbsolute } from 'node:path';
function safeJoin(workspace: string, rel: string): string {
  const safe = rel.trim();
  if (!safe || safe.split(/[\\/]+/).includes('..')) throw new Error('relativePath must not contain ..');
  const candidate = resolve(workspace, safe);
  const r = relative(workspace, candidate);
  if (r.startsWith('..') || isAbsolute(r)) throw new Error('path escapes workspace');
  return candidate;
}

Type guard

function isConfinedRelative(rel: unknown): rel is string {
  return typeof rel === 'string' && rel.trim() !== '' && !rel.trim().split(/[\\/]+/).includes('..');
}

Try / catch

try {
  return await fsRoute({ workspacePath, relativePath });
} catch (err) {
  if (err instanceof Error && err.message === 'Path escapes workspace') {
    // recompute against the real workspace directory instead of the root
    relativePath = path.relative(realWorkspaceDir, target);
    return await fsRoute({ workspacePath, relativePath });
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling routes backed by the { workspace, path: confinedPath, relativePath } handler with a relativePath whose resolution against the resolved workspace leaves it — typically 'a/..' collapsing to the workspace parent — or when workspace was resolved to a nested real directory and the relative path climbs above it.

Common situations: Sending '..' or 'dir/../..' style paths that pass string checks but escape after resolve; a workspace path containing symlinked segments so the real workspace is shallower than expected; client code assuming workspace == root and building relative paths that overshoot; navigating to a sibling directory via '../sibling'.

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/ac3ef1b439e7089d. Report an issue: GitHub.