{"record":{"id":"27c34ae31fb08fd3","repo":"ruvnet/ruflo","slug":"path-traversal-blocked-resolved","errorCode":null,"errorMessage":"Path traversal blocked: ${resolved}","messagePattern":"Path traversal blocked: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/hooks/src/workers/index.ts","lineNumber":57,"sourceCode":"  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 */\nfunction safePath(projectRoot: string, ...segments: string[]): string {\n  const resolved = path.resolve(projectRoot, ...segments);\n  const realRoot = path.resolve(projectRoot);\n\n  if (!resolved.startsWith(realRoot + path.sep) && resolved !== realRoot) {\n    throw new Error(`Path traversal blocked: ${resolved}`);\n  }\n  return resolved;\n}\n","sourceCodeStart":39,"sourceCodeEnd":75,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/hooks/src/workers/index.ts#L39-L75","documentation":"The second branch of safePathAsync(): when the target file does not exist yet, realpath() cannot resolve it, so the function validates the parent directory instead — resolving the parent with realpath and throwing this traversal error when the parent's real location is outside projectRoot. It exists so not-yet-created files (typical for write targets) get the same containment guarantee as existing ones.","triggerScenarios":"Requesting a write path whose parent directory resolves outside the root: segments with ../, an absolute parent elsewhere, or a parent directory that is a symlink pointing outside the workspace — and the final file itself does not exist yet, triggering the parent-check branch.","commonSituations":"Output directories configured with relative ../ paths that resolve differently depending on cwd; symlinked build/output folders (e.g. symlink to a shared artifacts volume); generated-file paths built from templates concatenated with user input; projectRoot mismatch between environments (CI checkout vs local).","solutions":["Express output paths strictly relative to projectRoot without '..' segments (e.g. 'output/cfg.json', not '../shared/cfg.json')","Ensure parent directories exist inside the root and are not symlinks escaping it; replace symlinked output dirs with real directories inside the workspace","Pre-validate candidate paths with your own containment check before invoking worker write APIs"],"exampleFix":"// before\nawait worker.write('../shared/cfg.json', data); // parent outside root -> blocked\n\n// after\nawait worker.write('shared/cfg.json', data); // stays inside projectRoot","handlingStrategy":"validation","validationCode":"// For write targets: validate the parent dir stays inside the root\nimport * as path from 'node:path';\nfunction isSafeWritePath(root: string, relPath: string): boolean {\n  const parent = path.dirname(path.resolve(root, relPath));\n  const rel = path.relative(path.resolve(root), parent);\n  return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));\n}","typeGuard":"function isRelativeInside(relPath: string): boolean {\n  return !path.isAbsolute(relPath) && relPath.split(path.sep).every((s) => s !== '..');\n}","tryCatchPattern":"try {\n  await worker.write(relPath, data);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Path traversal blocked')) {\n    // the parent dir resolved outside projectRoot — reject, do not sanitize by retry\n    throw new BadRequestError('write path must stay inside the workspace');\n  }\n  throw e;\n}","preventionTips":["Express generated-file paths as root-relative without '..' segments","Create output directories inside the workspace instead of symlinking to external volumes","Keep cwd-independent projectRoot so '../' in configs resolves consistently"],"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"}