{"record":{"id":"fd9c1cf95f9625ec","repo":"mastra-ai/mastra","slug":"path-is-outside-the-browsable-root","errorCode":null,"errorMessage":"Path is outside the browsable root","messagePattern":"Path is outside the browsable root","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/routes/fs.ts","lineNumber":238,"sourceCode":"  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);\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/**","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/routes/fs.ts#L220-L256","documentation":"confinedWorkspacePath resolves the configured workspace root, resolves the requested workspacePath (absolute or relative to that root), then follows symlinks via realPathWithinRoot to confirm the real location stays inside the root. It throws \"Path is outside the browsable root\" when the candidate path does not exist within the root or resolves (through symlinks) to a location outside it — an anti-traversal/anti-symlink-escape guard.","triggerScenarios":"Calling routes backed by the { workspace } handler with a workspacePath that points outside resolveFsRoot(root); a symlink inside the workspace pointing to e.g. /etc; a nonexistent path (realpath fails, returns null); or a root whose realpath differs from the configured root so candidate resolves under the non-real path.","commonSituations":"UI passing a bookmarked absolute path from a previous machine or different workspace; a symlinked directory (e.g. node_modules -> shared cache) inside the workspace; stale links after the workspace was moved or deleted; mounting the workspace via a symlinked path so resolvedRoot and candidate diverge.","solutions":["Pass a workspacePath that exists inside the configured root (verify with fs.realpath and check it starts with the root's realpath)","Remove or replace symlinks inside the workspace that point outside the root","Use the resolved root's realpath when constructing candidate paths (realOrResolved is applied to root; match it client-side)","If the target must be browsable, place it physically (or symlink-safe) inside the approved root"],"exampleFix":"// before\nconst res = await fetch(`/api/fs/list?workspacePath=${encodeURIComponent(linkToOutside)}`);\n// after\nconst real = await fs.realpath(target);\nconst rootReal = await fs.realpath(workspaceRoot);\nif (!real.startsWith(rootReal + path.sep)) throw new Error('outside browsable root');\nconst res = await fetch(`/api/fs/list?workspacePath=${encodeURIComponent(path.relative(rootReal, real))}`);","handlingStrategy":"validation","validationCode":"import { realpath, realpathSync } from 'node:fs/promises';\nasync function assertInsideRoot(p: string, root: string): Promise<string> {\n  const [realP, realRoot] = await Promise.all([realpath(p).catch(() => null), realpath(root)]);\n  if (!realP || !(realP === realRoot || realP.startsWith(realRoot + require('path').sep))) {\n    throw new Error('path is outside the browsable root');\n  }\n  return realP;\n}","typeGuard":"function isWithinRoot(candidate: string, root: string): boolean {\n  const rel = require('path').relative(root, candidate);\n  return rel === '' || (!rel.startsWith('..') && !require('path').isAbsolute(rel));\n}","tryCatchPattern":"try {\n  const listing = await browseRoute({ workspacePath });\n} catch (err) {\n  if (err instanceof Error && err.message === 'Path is outside the browsable root') {\n    workspacePath = '.'; // clamp to the configured root\n    return await browseRoute({ workspacePath });\n  }\n  throw err;\n}","preventionTips":["realpath both the target and the root before comparing (symlink-aware check)","Avoid symlinks inside workspaces that point outside the root","Verify the target exists before calling — missing paths also yield this error","Use paths derived from the root's realpath, since the server resolves the root first"],"tags":["path-traversal","symlink","security","filesystem"],"backgroundTag":"symlink-escape-blocked","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T03:17:51.788Z"}