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
- Compute the relative path against the actual workspace directory (the realpath), not the configured root, and ensure it never begins with '..'
- Navigate to sibling directories by requesting their workspace path directly instead of using '..'
- Use path.relative(realWorkspace, target) and reject results starting with '..' before the request
- 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
- Compute relative paths against the workspace realpath, not the configured root
- Never use '..' to reach siblings — request the sibling's workspace path directly
- Run assertRelativePath-equivalent checks client-side before every request
- Remember two layers apply: '..' segments (raw check) and post-resolve containment (realpath check) — satisfy both
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
- ${label} escapes workspace
- Invalid resourceId: ${resourceId}
- Invalid route path: "${path}". Path cannot contain '..', '?'
- Worker ${label} must stay within the deployed artifact root.
- ${label} must be relative
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/ac3ef1b439e7089d.
Report an issue: GitHub.