{"record":{"id":"35a94621137c102a","repo":"ruvnet/ruflo","slug":"path-traversal-blocked-realresolved","errorCode":null,"errorMessage":"Path traversal blocked: ${realResolved}","messagePattern":"Path traversal blocked: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/hooks/src/workers/index.ts","lineNumber":47,"sourceCode":"\n// ============================================================================\n// Security Utilities\n// ============================================================================\n\n/**\n * Validate and resolve a path ensuring it stays within projectRoot\n * Uses realpath to prevent TOCTOU symlink attacks\n */\nasync function safePathAsync(projectRoot: string, ...segments: string[]): Promise<string> {\n  const resolved = path.resolve(projectRoot, ...segments);\n\n  try {\n    // Resolve symlinks to prevent TOCTOU attacks\n    const realResolved = await fs.realpath(resolved).catch(() => resolved);\n    const realRoot = await fs.realpath(projectRoot).catch(() => projectRoot);\n\n    if (!realResolved.startsWith(realRoot + path.sep) && realResolved !== realRoot) {\n      throw new Error(`Path traversal blocked: ${realResolved}`);\n    }\n    return realResolved;\n  } catch (error) {\n    // If file doesn't exist yet, validate the parent directory\n    const parent = path.dirname(resolved);\n    const realParent = await fs.realpath(parent).catch(() => parent);\n    const realRoot = await fs.realpath(projectRoot).catch(() => projectRoot);\n\n    if (!realParent.startsWith(realRoot + path.sep) && realParent !== realRoot) {\n      throw new Error(`Path traversal blocked: ${resolved}`);\n    }\n    return resolved;\n  }\n}\n\n/**\n * Synchronous path validation (for non-async contexts)\n */","sourceCodeStart":29,"sourceCodeEnd":65,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/hooks/src/workers/index.ts#L29-L65","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nawait worker.write(userId + '/' + fileName, data);\n// fileName = '../../../etc/cron.d/pwn' -> blocked\n\n// after\nconst safeName = path.basename(fileName); // no separators, no '..'\nif (safeName !== fileName) throw new Error('fileName must not contain path separators');\nawait worker.write(userId + '/' + safeName, data);","handlingStrategy":"validation","validationCode":"// Pre-validate candidate paths with the same containment rule\nimport * as path from 'node:path';\nfunction isWithinRoot(root: string, candidate: string): boolean {\n  const rel = path.relative(path.resolve(root), path.resolve(root, candidate));\n  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}\nif (!isWithinRoot(projectRoot, requestedPath)) {\n  throw new Error(`rejected out-of-root path: ${requestedPath}`);\n}","typeGuard":"function isSafeFileName(name: string): boolean {\n  const base = path.basename(name);\n  return base === name && name !== '' && name !== '.' && name !== '..';\n}","tryCatchPattern":"try {\n  await worker.write(segments, data);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Path traversal blocked')) {\n    // security control: log the offending path and reject the request — never retry as-is\n    auditLog.warn('traversal attempt', { segments });\n    throw new BadRequestError('invalid path');\n  }\n  throw e;\n}","preventionTips":["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()"],"tags":["security","path-traversal","workers","hooks","filesystem","symlink"],"backgroundTag":"path-traversal-blocked","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}