{"record":{"id":"bd54f69bade91b47","repo":"n8n-io/n8n","slug":"path-traversal-detected-refusing-to-join-paths","errorCode":null,"errorMessage":"Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}","messagePattern":"Path traversal detected, refusing to join paths: (.+?) and (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"packages/@n8n/scan-community-package/scanner/scanner.mjs","lineNumber":47,"sourceCode":"\tif (parentPath === childPath) {\n\t\treturn true;\n\t}\n\n\treturn childPath.startsWith(parentPath + path.sep);\n}\n\n/**\n * Joins the given paths to the parentPath, ensuring that the resulting path\n * is still contained within the parentPath. If not, it throws an error to\n * prevent path traversal vulnerabilities.\n *\n * @throws {UnexpectedError} If the resulting path is not contained within the parentPath.\n */\nexport function safeJoinPath(parentPath, ...paths) {\n\tconst candidate = path.join(parentPath, ...paths);\n\n\tif (!isContainedWithin(parentPath, candidate)) {\n\t\tthrow new Error(\n\t\t\t`Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`,\n\t\t);\n\t}\n\n\treturn candidate;\n}\n\nexport const resolvePackage = (packageSpec) => {\n\t// Validate input to prevent command injection\n\tif (!/^[a-zA-Z0-9@/_.-]+$/.test(packageSpec)) {\n\t\tthrow new Error('Invalid package specification');\n\t}\n\n\tlet packageName, version;\n\tif (packageSpec.startsWith('@')) {\n\t\tif (packageSpec.includes('@', 1)) {\n\t\t\t// Handle scoped packages with versions\n\t\t\tconst lastAtIndex = packageSpec.lastIndexOf('@');","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/n8n-io/n8n/blob/5ac6606e81f67bb9534255570cd4e86fd8101eee/packages/@n8n/scan-community-package/scanner/scanner.mjs#L29-L65","documentation":"Thrown by safeJoinPath in the community-package scanner when path.join(parentPath, ...paths) produces a result that is NOT contained within parentPath. The guard exists to prevent path traversal: an attacker-controlled path component (e.g. '../../etc/passwd' or an absolute path) must not let the scanner read or write outside the sandboxed temp directory during package scanning.","triggerScenarios":"Calling safeJoinPath(TEMP_DIR, packageName, version) where packageName or a derived segment contains '..' segments, an absolute path, or a symlink that resolves outside TEMP_DIR. Also triggered if TEMP_DIR itself is a symlink and isContainedWithin compares lexical rather than resolved paths.","commonSituations":"A malicious community package whose name/version contains traversal characters; a registry tarball whose `package.json` name field resolves outside the extraction dir; running the scanner against an untrusted npm spec; TEMP_DIR on a path that includes symlinked components.","solutions":["Sanitize packageName and version upstream in resolvePackage (the regex at scanner.mjs:58 already rejects most traversal chars - extend it if a new vector slips through).","Use realpaths for both parentPath and candidate before the containment check so symlink-resolved paths are compared.","Reject any path segment equal to '..' or starting with '/' before calling safeJoinPath.","Run the scanner in a container with a read-only root and a throwaway TEMP_DIR so even a traversal has nowhere to go."],"exampleFix":"// before\nconst packageDir = safeJoinPath(TEMP_DIR, `${packageName}-${version}`);\n\n// after - normalize and reject traversal explicitly before joining\nconst segment = `${packageName}-${version}`;\nif (segment.includes('..') || path.isAbsolute(segment)) {\n  throw new Error(`Refusing suspicious package dir segment: ${segment}`);\n}\nconst packageDir = safeJoinPath(TEMP_DIR, segment);","handlingStrategy":"validation","validationCode":"import path from 'node:path';\n\nfunction isSafeSegment(segment: string): boolean {\n  if (segment.includes('..')) return false;\n  if (path.isAbsolute(segment)) return false;\n  return true;\n}\n\n// run before safeJoinPath\nfor (const p of paths) {\n  if (!isSafeSegment(String(p))) {\n    throw new Error(`Refusing unsafe path segment: ${String(p)}`);\n  }\n}","typeGuard":"function isContained(parent: string, child: string): boolean {\n  const rel = path.relative(parent, child);\n  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}","tryCatchPattern":"try {\n  const dir = safeJoinPath(TEMP_DIR, segment);\n} catch (e) {\n  // Treat as untrusted input - log the segment, reject the package, do NOT retry with normalization.\n  logger.warn('Rejecting package with unsafe path segment', { segment, err: (e as Error).message });\n  throw new Error('Package rejected: path traversal in name or version');\n}","preventionTips":["Treat every package-derived string (name, version, directory) as untrusted and reject '..' / absolute segments before joining.","Use realpathSync on both parent and candidate before the containment check so symlinks cannot escape.","Run the scanner in a sandbox (container/namespace) so even a successful traversal has no privileged target."],"tags":["security","path-traversal","scanner","filesystem"],"backgroundTag":null,"analyzedSha":"5ac6606e81f67bb9534255570cd4e86fd8101eee","analyzedAt":"2026-08-12T05:26:35.080Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}