{"record":{"id":"6b68bfe09812cc24","repo":"different-ai/openwork","slug":"path-escape","errorCode":"path_escape","errorMessage":"Path escapes workspace root","messagePattern":"Path escapes workspace root","errorType":"error_code","errorClass":"ApiError","httpStatus":400,"severity":"error","filePath":"apps/server/src/paths.ts","lineNumber":17,"sourceCode":"import { realpath } from \"node:fs/promises\";\nimport { isAbsolute, resolve, sep } from \"node:path\";\nimport { ApiError } from \"./errors.js\";\n\nexport function assertAbsolute(path: string): void {\n  if (!isAbsolute(path)) {\n    throw new ApiError(400, \"invalid_path\", \"Path must be absolute\");\n  }\n}\n\nexport async function resolveWithinRoot(root: string, ...segments: string[]): Promise<string> {\n  const resolvedRoot = await realpath(root);\n  const candidate = resolve(resolvedRoot, ...segments);\n  const resolvedCandidate = await realpath(candidate).catch(() => candidate);\n  if (resolvedCandidate === resolvedRoot) return candidate;\n  if (!resolvedCandidate.startsWith(resolvedRoot + sep)) {\n    throw new ApiError(400, \"path_escape\", \"Path escapes workspace root\");\n  }\n  return candidate;\n}\n","sourceCodeStart":1,"sourceCodeEnd":21,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/paths.ts#L1-L21","documentation":"resolveWithinRoot resolves requested segments under the workspace root, resolves symlinks via realpath, and throws this 400 ApiError (code path_escape) when the resolved candidate lands outside the root. It prevents path traversal (../) and symlink escape from reading/writing outside the workspace.","triggerScenarios":"filePath (or another resolveWithinRoot caller) invoked with segments containing ../ that escape the root, or a symlink inside the workspace pointing to an external target.","commonSituations":"Path traversal attempts on the attachments/file API; symlinks created inside workspaces pointing at home dirs; clients joining user input into paths without normalization.","solutions":["Remove ../ segments and pass paths that stay inside the workspace root.","Remove or relocate symlinks that point outside the workspace.","Normalize/canonicalize the requested path client-side and reject escapes before calling.","If external files are legitimately needed, copy or mount them inside the workspace rather than linking."],"exampleFix":"// before: unsanitized user input\nawait api.filePath({ path: resolve(root, userInput) }); // userInput = \"../../etc/passwd\"\n// after: validate containment first\nconst candidate = resolve(root, userInput);\nif (!candidate.startsWith(root + sep)) throw new ApiError(400, \"invalid_path\", \"Path must stay inside workspace\");\nawait api.filePath({ path: candidate });","handlingStrategy":"validation","validationCode":"import { resolve, sep, isAbsolute } from \"node:path\";\nfunction staysWithinRoot(root: string, ...segments: string[]): boolean {\n  const candidate = resolve(root, ...segments);\n  return candidate === root || candidate.startsWith(root + sep);\n}","typeGuard":"function isInsideWorkspace(root: string, p: string): boolean {\n  if (!isAbsolute(p)) return false;\n  const rel = relative(root, p);\n  return rel === \"\" || (!rel.startsWith(\"..\") && !isAbsolute(rel));\n}","tryCatchPattern":"import { ApiError } from \"./errors.js\";\ntry {\n  const path = await resolveWithinRoot(root, userInput);\n} catch (err) {\n  if (err instanceof ApiError && err.code === \"path_escape\") {\n    throw new ApiError(400, \"path_escape\", \"Requested path is outside the workspace and was blocked.\");\n  }\n  throw err;\n}","preventionTips":["Normalize user-supplied path segments (strip .., resolve) before sending to the API.","Avoid creating symlinks inside workspaces that point to external locations.","Never join raw user input into paths; route through a containment check first.","Treat path_escape from untrusted clients as a traversal attempt — log it.","Remember realpath is applied: even valid-looking paths escape if a symlink target is outside the root."],"tags":["path","security","path-traversal","sandbox"],"backgroundTag":"path-traversal-blocked","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}