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

  1. Pass simple relative filenames that stay inside projectRoot; strip directory components from user input (path.basename) before calling worker APIs
  2. Compute projectRoot from a stable anchor (the package/worktree root) rather than process.cwd() which can differ per invocation
  3. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/35a94621137c102a. Report an issue: GitHub.