n8n-io/n8n · critical · Error

Path traversal detected, refusing to join paths

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.

Solutions

  1. 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).
  2. Use realpaths for both parentPath and candidate before the containment check so symlink-resolved paths are compared.
  3. Reject any path segment equal to '..' or starting with '/' before calling safeJoinPath.
  4. 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

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


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/bd54f69bade91b47. Report an issue: GitHub.

Appendix: 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)