{"record":{"id":"ac3ef1b439e7089d","repo":"mastra-ai/mastra","slug":"path-escapes-workspace","errorCode":null,"errorMessage":"Path escapes workspace","messagePattern":"Path escapes workspace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/routes/fs.ts","lineNumber":250,"sourceCode":"  root: string,\n  workspacePath: string,\n): Promise<{ resolvedRoot: string; workspace: string }> {\n  const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n  const candidate = isAbsolute(workspacePath) ? resolve(workspacePath) : resolve(resolvedRoot, workspacePath);\n  const workspace = await realPathWithinRoot(candidate, resolvedRoot);\n  if (!workspace) throw new Error('Path is outside the browsable root');\n  return { resolvedRoot, workspace };\n}\n\nasync function confinedWorkspaceRelativePath(\n  root: string,\n  workspacePath: string,\n  relativePath: string,\n): Promise<{ workspace: string; path: string; relativePath: string }> {\n  const safeRelativePath = assertRelativePath(relativePath, 'path');\n  const { workspace } = await confinedWorkspacePath(root, workspacePath);\n  const candidate = resolve(workspace, safeRelativePath);\n  if (!isWithinRoot(candidate, workspace)) throw new Error('Path escapes workspace');\n  const confinedPath = await realPathWithinRoot(candidate, workspace);\n  if (!confinedPath) throw new Error('Path is outside the workspace');\n  return { workspace, path: confinedPath, relativePath: safeRelativePath };\n}\n\n/**\n * List the directories inside `requestedPath`, confined to `root`. An absent or\n * out-of-root path is clamped to the root, so the worst a malicious client can\n * do is browse within the allowed root.\n */\nexport async function listDirectory(root: string, requestedPath?: string): Promise<DirectoryListing> {\n  // Resolve the root through symlinks so all confinement checks compare real\n  // paths; a symlink that escapes the root is then reliably detectable.\n  const resolvedRoot = await realOrResolved(resolveFsRoot(root));\n\n  let target = resolvedRoot;\n  if (requestedPath && requestedPath.trim()) {\n    const candidate = isAbsolute(requestedPath) ? resolve(requestedPath) : resolve(resolvedRoot, requestedPath);","sourceCodeStart":232,"sourceCodeEnd":268,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/routes/fs.ts#L232-L268","documentation":"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).","triggerScenarios":"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.","commonSituations":"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'.","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"],"exampleFix":"// before\nconst rel = '../shared/config.json';\n// after\nconst rel = path.relative(realWorkspaceDir, targetFile);\nif (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error('target outside workspace');\nconst res = await fetch(`/api/fs/read?workspacePath=${ws}&path=${encodeURIComponent(rel)}`);","handlingStrategy":"validation","validationCode":"import { resolve, relative, isAbsolute } from 'node:path';\nfunction safeJoin(workspace: string, rel: string): string {\n  const safe = rel.trim();\n  if (!safe || safe.split(/[\\\\/]+/).includes('..')) throw new Error('relativePath must not contain ..');\n  const candidate = resolve(workspace, safe);\n  const r = relative(workspace, candidate);\n  if (r.startsWith('..') || isAbsolute(r)) throw new Error('path escapes workspace');\n  return candidate;\n}","typeGuard":"function isConfinedRelative(rel: unknown): rel is string {\n  return typeof rel === 'string' && rel.trim() !== '' && !rel.trim().split(/[\\\\/]+/).includes('..');\n}","tryCatchPattern":"try {\n  return await fsRoute({ workspacePath, relativePath });\n} catch (err) {\n  if (err instanceof Error && err.message === 'Path escapes workspace') {\n    // recompute against the real workspace directory instead of the root\n    relativePath = path.relative(realWorkspaceDir, target);\n    return await fsRoute({ workspacePath, relativePath });\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["path-traversal","security","validation","filesystem"],"backgroundTag":"path-traversal-blocked","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}