{"record":{"id":"5ed6c4cb1d0d1d6f","repo":"mastra-ai/mastra","slug":"root-escapes-workspace","errorCode":null,"errorMessage":"Root escapes workspace","messagePattern":"Root escapes workspace","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/routes/fs.ts","lineNumber":345,"sourceCode":"        type: 'file',\n        size: info.size,\n        updatedAt: info.mtime.toISOString(),\n      });\n    }\n  }\n\n  return entries.sort((a, b) => a.path.localeCompare(b.path));\n}\n\nexport async function listWorkspaceRenderedPath(\n  root: string,\n  workspacePath: string,\n  renderedRoot: string,\n): Promise<WorkspaceRenderedListing> {\n  const safeRoot = assertApprovedRenderedRoot(renderedRoot);\n  const { workspace } = await confinedWorkspacePath(root, workspacePath);\n  const renderedPath = resolve(workspace, safeRoot);\n  if (!isWithinRoot(renderedPath, workspace)) throw new Error('Root escapes workspace');\n\n  const confinedRootPath = await realPathWithinRoot(renderedPath, workspace);\n  if (!confinedRootPath) return { workspacePath: workspace, root: safeRoot, rootPath: renderedPath, entries: [] };\n\n  const info = await stat(confinedRootPath);\n  if (!info.isDirectory()) return { workspacePath: workspace, root: safeRoot, rootPath: confinedRootPath, entries: [] };\n\n  return {\n    workspacePath: workspace,\n    root: safeRoot,\n    rootPath: confinedRootPath,\n    entries: await listRenderedEntries(confinedRootPath),\n  };\n}\n\nexport async function readWorkspaceFile(root: string, workspacePath: string, path: string): Promise<WorkspaceFile> {\n  const safePath = assertRelativePath(path, 'path');\n  const relativeRoot = safePath.split('/')[0] ?? '';","sourceCodeStart":327,"sourceCodeEnd":363,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/routes/fs.ts#L327-L363","documentation":"listWorkspaceRenderedPath resolves an approved rendered root (e.g. '.artifacts') against the workspace directory and then checks the computed absolute path stays within the workspace. 'Root escapes workspace' is thrown when resolving the approved root name against the workspace produces a path outside the workspace — practically only possible if the approved-root allowlist or the workspace resolution changed, since safeRoot has already been validated as relative and '..'-free. It is a defense-in-depth containment check before any filesystem access.","triggerScenarios":"Calling listWorkspaceRenderedPath with a renderedRoot that resolves outside the workspace. With the current allowlist this is nearly unreachable from user input (assertApprovedRenderedRoot rejects non-approved roots and any '..'), so hits usually mean a modified APPROVED_RENDERED_ROOTS entry, a mutated resolve/isWithinRoot helper, or a workspace resolution inconsistency.","commonSituations":"A developer adding a new entry to APPROVED_RENDERED_ROOTS that contains a traversal or absolute component; a symlink at workspace/.artifacts pointing outside the workspace combined with a helper bypass; custom forks changing the confinement helpers.","solutions":["Check the renderedRoot argument — it must be one of the approved roots (e.g. '.artifacts') and contain no '..' or absolute components.","Inspect APPROVED_RENDERED_ROOTS and remove/fix any entry that could resolve outside the workspace.","Confirm the workspace itself resolved correctly (confinedWorkspacePath) — an empty or odd workspacePath can make resolve() land somewhere unexpected.","Ensure .artifacts (or the relevant root) is a real directory inside the workspace, not a symlink out of it."],"exampleFix":"// before\nAPPROVED_RENDERED_ROOTS = new Set(['.artifacts', '../shared-out'])\n// after\nAPPROVED_RENDERED_ROOTS = new Set(['.artifacts'])","handlingStrategy":"validation","validationCode":"import { resolve } from 'node:path';\nconst APPROVED = new Set(['.artifacts']);\nfunction isApprovedRoot(root: string): boolean {\n  const t = root.trim();\n  return APPROVED.has(t) && !t.split(/[\\\\/]+/).includes('..') && resolve('/workspace', t).startsWith('/workspace');\n}\nif (!isApprovedRoot(renderedRoot)) throw new Error('refusing request: root not approved');","typeGuard":"function isApprovedRenderedRoot(root: string): root is '.artifacts' {\n  return root === '.artifacts';\n}","tryCatchPattern":"try {\n  const listing = await listWorkspaceRenderedPath(root, ws, renderedRoot);\n} catch (e) {\n  if (e instanceof Error && e.message === 'Root escapes workspace') {\n    // containment violation: log and return empty listing\n    return { workspacePath: ws, root: renderedRoot, entries: [] };\n  }\n  throw e;\n}","preventionTips":["Only pass literal approved root names (e.g. '.artifacts'), never user-supplied or concatenated strings.","Keep the APPROVED_RENDERED_ROOTS allowlist free of traversal or absolute entries; add a unit test asserting each entry resolves inside the workspace.","Ensure the rendered root is a real directory inside the workspace, not an outbound symlink.","Don't modify confinement helpers (resolve/isWithinRoot) without re-running the containment tests."],"tags":["filesystem","path-traversal","security","containment"],"backgroundTag":"path-escapes-workspace","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}