{"record":{"id":"110b04c764bb18a0","repo":"mastra-ai/mastra","slug":"root-is-not-approved-for-rendered-workspace-access","errorCode":null,"errorMessage":"Root is not approved for rendered workspace access","messagePattern":"Root is not approved for rendered workspace access","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"mastracode/factory/src/routes/fs.ts","lineNumber":227,"sourceCode":"  } 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);\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,","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/mastra-ai/mastra/blob/75dd419e613fe9c39f846ffc500716141b74fda6/mastracode/factory/src/routes/fs.ts#L209-L245","documentation":"assertApprovedRenderedRoot validates that a requested workspace root (after relative-path sanitization) is one of the pre-approved rendered roots in APPROVED_RENDERED_ROOTS. The factory throws \"Root is not approved for rendered workspace access\" when the caller asks the fs routes to serve a root that the deployment has not explicitly allow-listed, preventing arbitrary directories from being exposed for rendered output browsing.","triggerScenarios":"Calling routes that use safeRoot, readWorkspaceFile, or readSessionWorkspaceFile with ?root=<something> whose sanitized value is not present in APPROVED_RENDERED_ROOTS — e.g. a typo'd root name, a session-specific directory not on the list, or a root added to the filesystem but not to the allow-list constant.","commonSituations":"New workspace/session directories created by the pipeline but APPROVED_RENDERED_ROOTS not updated; case or trailing-slash mismatches after sanitization; deploying code with a hardcoded root name that differs per environment; tests referencing fixture roots not in the allow-list.","solutions":["Use one of the approved root values exactly as listed in APPROVED_RENDERED_ROOTS in mastracode/factory/src/routes/fs.ts","If a new root is legitimately needed, add its sanitized relative form to APPROVED_RENDERED_ROOTS and redeploy","Log/inspect the sanitized root (assertRelativePath output) to spot mismatched case, slashes, or nesting","Verify the route is the right one: rendered-workspace routes only accept rendered roots; generic browsing routes take the configured workspace root instead"],"exampleFix":"// before\nconst res = await fetch('/api/fs/rendered?root=sessions/tmp-123/output');\n// after (add to APPROVED_RENDERED_ROOTS first, or use an approved root)\nconst APPROVED = ['rendered', 'sessions/rendered'];\nconst root = APPROVED.includes('rendered') ? 'rendered' : APPROVED[0];\nconst res = await fetch(`/api/fs/rendered?root=${root}`);","handlingStrategy":"validation","validationCode":"const APPROVED_RENDERED_ROOTS = new Set(['rendered']); // mirror of the server-side set\nfunction useApprovedRoot(root: string): string {\n  const safe = root.trim().replace(/^\\/+/, '');\n  if (!APPROVED_RENDERED_ROOTS.has(safe)) throw new Error(`root not approved: ${safe}`);\n  return safe;\n}","typeGuard":"function isApprovedRoot(root: unknown): root is string {\n  return typeof root === 'string' && APPROVED_RENDERED_ROOTS.has(root.trim().replace(/^\\/+/, ''));\n}","tryCatchPattern":"try {\n  return await renderedRoute({ root });\n} catch (err) {\n  if (err instanceof Error && err.message.includes('not approved for rendered workspace')) {\n    root = 'rendered'; // fall back to the default approved root\n    return await renderedRoute({ root });\n  }\n  throw err;\n}","preventionTips":["Keep a shared constant of approved roots in sync between client and server","When adding new workspace roots, update APPROVED_RENDERED_ROOTS in the same PR","Match the sanitized form exactly (no leading/trailing slashes, correct case)","Prefer the default approved root unless a new one was explicitly approved"],"tags":["security","allowlist","configuration","filesystem"],"backgroundTag":"root-not-allowlisted","analyzedSha":"75dd419e613fe9c39f846ffc500716141b74fda6","analyzedAt":"2026-08-30T00:15:31.844Z","schemaVersion":2},"datasetVersion":"2026-08-30T08:17:16.595Z"}