{"record":{"id":"cb180dcd537a35b9","repo":"mastra-ai/mastra","slug":"label-escapes-workspace","errorCode":null,"errorMessage":"${label} escapes workspace","messagePattern":"(.+?) escapes workspace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/routes/fs.ts","lineNumber":218,"sourceCode":" * Resolve a path's real location (following symlinks) and confirm it stays\n * within `root`. Returns the real path when confined, or `null` when it escapes\n * the root or does not exist. Used so a symlink inside the root that points\n * outside it cannot be browsed or selected.\n */\nasync function realPathWithinRoot(candidate: string, root: string): Promise<string | null> {\n  try {\n    const real = await realpath(candidate);\n    return isWithinRoot(real, root) ? real : null;\n  } catch {\n    return null;\n  }\n}\n\nfunction assertRelativePath(path: string, label: string): string {\n  const trimmed = path.trim();\n  if (!trimmed) throw new Error(`Missing required query param: ${label}`);\n  if (isAbsolute(trimmed)) throw new Error(`${label} must be relative`);\n  if (trimmed.split(/[\\\\/]+/).includes('..')) throw new Error(`${label} escapes workspace`);\n  const normalized = resolve('/', trimmed).slice(1);\n  if (!normalized || normalized === '..' || normalized.startsWith(`..${sep}`))\n    throw new Error(`${label} escapes workspace`);\n  return normalized;\n}\n\nfunction assertApprovedRenderedRoot(renderedRoot: string): string {\n  const safeRoot = assertRelativePath(renderedRoot, 'root');\n  if (!APPROVED_RENDERED_ROOTS.has(safeRoot)) throw new Error('Root is not approved for rendered workspace access');\n  return safeRoot;\n}\n\nasync function confinedWorkspacePath(\n  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);","sourceCodeStart":200,"sourceCodeEnd":236,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/routes/fs.ts#L200-L236","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Remove '..' segments from the path before sending; navigate within the workspace using paths relative to its root","Use path.resolve(workspaceRoot, requested) in the caller and verify the result starts with workspaceRoot before passing it as the query param","Pass an absolute workspacePath via the dedicated workspace param (which is confined via confinedWorkspacePath) instead of escaping a relative one","If upward navigation is needed, request the workspace root itself rather than '..'"],"exampleFix":"// before\nconst res = await fetch(`/api/fs?path=${encodeURIComponent('../other-project/file.txt')}`);\n// after\nconst rel = path.relative(workspaceRoot, targetFile);\nif (rel.startsWith('..')) throw new Error('target is outside workspace');\nconst res = await fetch(`/api/fs?path=${encodeURIComponent(rel)}`);","handlingStrategy":"validation","validationCode":"import { isAbsolute, resolve, sep } from 'node:path';\nfunction isSafeRelative(p: string): boolean {\n  const t = p.trim();\n  if (!t || isAbsolute(t)) return false;\n  if (t.split(/[\\\\/]+/).includes('..')) return false;\n  const norm = resolve('/', t).slice(1);\n  return !!norm && norm !== '..' && !norm.startsWith(`..${sep}`);\n}\nif (!isSafeRelative(userPath)) throw new Error('path escapes workspace');","typeGuard":"function isSafeRelativePath(p: unknown): p is string {\n  return typeof p === 'string' && isSafeRelative(p);\n}","tryCatchPattern":"try {\n  const data = await fetchFsRoute({ path: userPath });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('escapes workspace')) {\n    // clamp to workspace root or re-derive with path.relative(root, target)\n  } else throw err;\n}","preventionTips":["Always build paths with path.relative(workspaceRoot, target) instead of string manipulation","Reject '..' segments client-side before any API call","Use resolve() and an isWithinRoot prefix check as a habit","Never concatenate user input into path query params unchecked"],"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"}