n8n-io/n8n · critical · Error
Path traversal detected, refusing to join paths: ${parentPat
Error message
Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)} What it means
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.
Source
Thrown at packages/@n8n/scan-community-package/scanner/scanner.mjs:47
if (parentPath === childPath) {
return true;
}
return childPath.startsWith(parentPath + path.sep);
}
/**
* Joins the given paths to the parentPath, ensuring that the resulting path
* is still contained within the parentPath. If not, it throws an error to
* prevent path traversal vulnerabilities.
*
* @throws {UnexpectedError} If the resulting path is not contained within the parentPath.
*/
export function safeJoinPath(parentPath, ...paths) {
const candidate = path.join(parentPath, ...paths);
if (!isContainedWithin(parentPath, candidate)) {
throw new Error(
`Path traversal detected, refusing to join paths: ${parentPath} and ${JSON.stringify(paths)}`,
);
}
return candidate;
}
export const resolvePackage = (packageSpec) => {
// Validate input to prevent command injection
if (!/^[a-zA-Z0-9@/_.-]+$/.test(packageSpec)) {
throw new Error('Invalid package specification');
}
let packageName, version;
if (packageSpec.startsWith('@')) {
if (packageSpec.includes('@', 1)) {
// Handle scoped packages with versions
const lastAtIndex = packageSpec.lastIndexOf('@');View on GitHub (pinned to 5ac6606e81)
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.
Example fix
// before
const packageDir = safeJoinPath(TEMP_DIR, `${packageName}-${version}`);
// after - normalize and reject traversal explicitly before joining
const segment = `${packageName}-${version}`;
if (segment.includes('..') || path.isAbsolute(segment)) {
throw new Error(`Refusing suspicious package dir segment: ${segment}`);
}
const packageDir = safeJoinPath(TEMP_DIR, segment); Defensive patterns
Strategy: validation
Validate before calling
import path from 'node:path';
function isSafeSegment(segment: string): boolean {
if (segment.includes('..')) return false;
if (path.isAbsolute(segment)) return false;
return true;
}
// run before safeJoinPath
for (const p of paths) {
if (!isSafeSegment(String(p))) {
throw new Error(`Refusing unsafe path segment: ${String(p)}`);
}
} Type guard
function isContained(parent: string, child: string): boolean {
const rel = path.relative(parent, child);
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
} Try / catch
try {
const dir = safeJoinPath(TEMP_DIR, segment);
} catch (e) {
// Treat as untrusted input - log the segment, reject the package, do NOT retry with normalization.
logger.warn('Rejecting package with unsafe path segment', { segment, err: (e as Error).message });
throw new Error('Package rejected: path traversal in name or version');
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Invalid skill at ${sourceDirectory}: ${errors.join('; ')}
- Access denied: "${excludedSegment}" is excluded from filesys
- Path "${relativePath}" escapes the base directory
- Access denied: cannot access "${relativePath}"
- Pattern "${pattern}" escapes the base directory
AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12).
Data as JSON: /api/errors/bd54f69bade91b47.
Report an issue: GitHub.