{"record":{"id":"05dbecdb4a399dd4","repo":"n8n-io/n8n","slug":"label-path-must-stay-within-the-workspace-root","errorCode":null,"errorMessage":"${label} path must stay within the workspace root: ${path}. Pass a workspace-relative path like src/workflows/my-workflow.workflow.ts.","messagePattern":"(.+?) path must stay within the workspace root: (.+?)\\. Pass a workspace-relative path like src/workflows/my-workflow\\.workflow\\.ts\\.","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/@n8n/instance-ai/src/workspace/workspace-paths.ts","lineNumber":41,"sourceCode":"\toptions: NormalizeWorkspaceRelativePathOptions = {},\n): string {\n\tconst label = options.resourceLabel ?? 'Workspace';\n\tlet trimmed = path.trim().replace(/^\\.\\/+/, '');\n\tif (options.workspaceRoot && trimmed.startsWith('/')) {\n\t\ttrimmed = stripWorkspaceRootPrefix(trimmed, options.workspaceRoot);\n\t}\n\tconst segments = trimmed.split('/');\n\tconst normalized = segments.filter((segment) => segment.length > 0 && segment !== '.').join('/');\n\n\tif (\n\t\tnormalized.length === 0 ||\n\t\ttrimmed.startsWith('/') ||\n\t\ttrimmed.startsWith('~/') ||\n\t\ttrimmed.includes('\\\\') ||\n\t\ttrimmed.includes('\\0') ||\n\t\tsegments.some((segment) => segment === '..')\n\t) {\n\t\tthrow new Error(\n\t\t\t`${label} path must stay within the workspace root: ${path}. ` +\n\t\t\t\t'Pass a workspace-relative path like src/workflows/my-workflow.workflow.ts.',\n\t\t);\n\t}\n\n\treturn normalized;\n}\n\nexport function joinWorkspacePath(root: string, path: string): string {\n\tconst normalizedRoot = root.replace(/\\/+$/, '') || '/';\n\tconst normalizedPath = normalizeWorkspaceRelativePath(path);\n\n\treturn normalizedRoot === '/' ? `/${normalizedPath}` : `${normalizedRoot}/${normalizedPath}`;\n}\n","sourceCodeStart":23,"sourceCodeEnd":56,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/instance-ai/src/workspace/workspace-paths.ts#L23-L56","documentation":"Thrown by normalizeWorkspaceRelativePath to enforce workspace containment. A path is rejected if, after stripping an allowed workspaceRoot prefix and collapsing segments, it is empty, starts with '/', starts with '~/', contains a backslash, contains a NUL byte, or has any '..' segment. This is the security guard that keeps AI-emitted file paths inside the sandbox workspace root.","triggerScenarios":"Calling normalizeWorkspaceRelativePath(path) or joinWorkspacePath(root, path) with: an absolute path like '/etc/passwd' that isn't under the configured workspaceRoot; a path containing '..' like 'src/../../secret'; a Windows path like 'src\\\\file'; an empty/whitespace-only string; a home-dir-relative '~/foo' path; or a path with a NUL byte injection attempt.","commonSituations":"An LLM tool emits an absolute sandbox path it saw in earlier shell output, but workspaceRoot was not set (or set to a different root) so the prefix strip fails; a model tries to read '/root/...' without the workspaceRoot option; a path with Windows separators is passed from a cross-platform caller; prompt injection inserts '..' to escape.","solutions":["Pass the absolute workspace root via options.workspaceRoot so echoed absolute paths under that root are auto-stripped to relative.","Use a workspace-relative path like 'src/workflows/my-workflow.workflow.ts' and never pass absolute paths.","Strip any leading './' and trailing '/' before calling — the function handles './' but not all variants.","Reject or sanitize model-emitted paths that contain '..' or backslashes before calling normalizeWorkspaceRelativePath."],"exampleFix":"// before — model emitted an absolute path\nconst rel = normalizeWorkspaceRelativePath('/home/user/workspace/src/file.ts');\n// throws\n\n// after — pass the workspace root so the prefix is stripped\nconst rel = normalizeWorkspaceRelativePath(\n  '/home/user/workspace/src/file.ts',\n  { workspaceRoot: '/home/user/workspace' },\n);\n// returns 'src/file.ts'","handlingStrategy":"validation","validationCode":"import { normalizeWorkspaceRelativePath } from './workspace/workspace-paths';\n\nfunction safeRelativePath(rawPath: string, workspaceRoot?: string): string | null {\n  try {\n    return normalizeWorkspaceRelativePath(rawPath, { workspaceRoot });\n  } catch {\n    return null;\n  }\n}","typeGuard":"function isSafeWorkspacePath(path: string, workspaceRoot?: string): boolean {\n  try {\n    normalizeWorkspaceRelativePath(path, { workspaceRoot });\n    return true;\n  } catch {\n    return false;\n  }\n}","tryCatchPattern":null,"preventionTips":["Always pass options.workspaceRoot when accepting model-emitted paths so absolute paths under the root are auto-stripped.","Validate paths with normalizeWorkspaceRelativePath in a try/catch before using them, returning a clear error to the model.","Never concatenate user/model input directly into sandbox shell commands — always go through joinWorkspacePath."],"tags":["security","path-traversal","validation","workspace","instance-ai"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}