ruvnet/ruflo · error
Path traversal blocked: ${realResolved}
Error message
Path traversal blocked: ${realResolved} What it means
safePathAsync() in the hooks workers module resolves worker file paths against projectRoot using fs.realpath on both the resolved path and the root, then throws this security error when the real path falls outside the project root. The realpath step specifically defeats TOCTOU symlink attacks where a path inside the root is swapped for a symlink pointing outside. This is a deliberate security control firing, not a bug.
Source
Thrown at v3/@claude-flow/hooks/src/workers/index.ts:47
// ============================================================================
// Security Utilities
// ============================================================================
/**
* Validate and resolve a path ensuring it stays within projectRoot
* Uses realpath to prevent TOCTOU symlink attacks
*/
async function safePathAsync(projectRoot: string, ...segments: string[]): Promise<string> {
const resolved = path.resolve(projectRoot, ...segments);
try {
// Resolve symlinks to prevent TOCTOU attacks
const realResolved = await fs.realpath(resolved).catch(() => resolved);
const realRoot = await fs.realpath(projectRoot).catch(() => projectRoot);
if (!realResolved.startsWith(realRoot + path.sep) && realResolved !== realRoot) {
throw new Error(`Path traversal blocked: ${realResolved}`);
}
return realResolved;
} catch (error) {
// If file doesn't exist yet, validate the parent directory
const parent = path.dirname(resolved);
const realParent = await fs.realpath(parent).catch(() => parent);
const realRoot = await fs.realpath(projectRoot).catch(() => projectRoot);
if (!realParent.startsWith(realRoot + path.sep) && realParent !== realRoot) {
throw new Error(`Path traversal blocked: ${resolved}`);
}
return resolved;
}
}
/**
* Synchronous path validation (for non-async contexts)
*/View on GitHub (pinned to fa13ee4ad6)
Solutions
- Pass simple relative filenames that stay inside projectRoot; strip directory components from user input (path.basename) before calling worker APIs
- Compute projectRoot from a stable anchor (the package/worktree root) rather than process.cwd() which can differ per invocation
- Remove or relocate symlinks that legitimately point outside the root, or copy the target content inside the workspace
Example fix
// before
await worker.write(userId + '/' + fileName, data);
// fileName = '../../../etc/cron.d/pwn' -> blocked
// after
const safeName = path.basename(fileName); // no separators, no '..'
if (safeName !== fileName) throw new Error('fileName must not contain path separators');
await worker.write(userId + '/' + safeName, data); Defensive patterns
Strategy: validation
Validate before calling
// Pre-validate candidate paths with the same containment rule
import * as path from 'node:path';
function isWithinRoot(root: string, candidate: string): boolean {
const rel = path.relative(path.resolve(root), path.resolve(root, candidate));
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}
if (!isWithinRoot(projectRoot, requestedPath)) {
throw new Error(`rejected out-of-root path: ${requestedPath}`);
} Type guard
function isSafeFileName(name: string): boolean {
const base = path.basename(name);
return base === name && name !== '' && name !== '.' && name !== '..';
} Try / catch
try {
await worker.write(segments, data);
} catch (e) {
if (e instanceof Error && e.message.startsWith('Path traversal blocked')) {
// security control: log the offending path and reject the request — never retry as-is
auditLog.warn('traversal attempt', { segments });
throw new BadRequestError('invalid path');
}
throw e;
} Prevention
- Never pass raw user input as path segments; map user IDs to server-generated basenames
- Reject absolute paths and '..' at the API boundary before workers see them
- Audit the workspace for symlinks pointing outside projectRoot and remove them
- Pin projectRoot to an explicit constant instead of process.cwd()
When it happens
Trigger: Passing path segments containing ../ that escape the root; passing an absolute path to another directory; or passing a path that traverses a symlink whose real target lives outside projectRoot (the resolved+realpath'd location fails the startsWith(realRoot) check).
Common situations: User-supplied filenames passed unsanitized into worker APIs; symlinks inside the workspace pointing to shared/temp directories outside it (pnpm node_modules, /tmp caches); projectRoot computed from the wrong cwd so legitimate paths suddenly look external; test fixtures using absolute paths.
Related errors
- Path traversal blocked: ${resolved}
- localSingleEntryPageRank: sourceIndex ${src} out of range [0
- AI job registry is a symlink (refusing): ${path}
- AI budget file is a symlink (refusing): ${path}
- Repo-supervisor file is a symlink (refusing): ${path}
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/35a94621137c102a.
Report an issue: GitHub.