{"record":{"id":"8e3ee80241a183c1","repo":"ruvnet/ruflo","slug":"unsafe-repository-relative-path-path","errorCode":null,"errorMessage":"unsafe repository-relative path: ${path}","messagePattern":"unsafe repository-relative path: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/codex/src/harness/repository-state.ts","lineNumber":207,"sourceCode":"  }\n  assertUnicodeScalarString(path);\n  if (path !== path.normalize('NFC')) {\n    throw new Error(`Git path is not NFC-normalized: ${path}`);\n  }\n  return normalizeRelativePath(path);\n}\n\nfunction normalizeRelativePath(path: string): string {\n  assertUnicodeScalarString(path);\n  if (path.includes('\\\\')) throw new Error(`ambiguous repository path separator: ${path}`);\n  const normalized = path.normalize('NFC');\n  if (\n    normalized.length === 0\n    || isAbsolute(normalized)\n    || normalized.startsWith('-')\n    || normalized.split('/').some((part) => part === '' || part === '.' || part === '..')\n  ) {\n    throw new Error(`unsafe repository-relative path: ${path}`);\n  }\n  return normalized;\n}\n\nfunction assertNoPathCollisions(paths: readonly string[]): void {\n  const exact = new Set<string>();\n  const folded = new Map<string, string>();\n  for (const path of paths) {\n    if (exact.has(path)) throw new Error(`duplicate repository path: ${path}`);\n    exact.add(path);\n    const key = portableCaseFold(path);\n    const prior = folded.get(key);\n    if (prior !== undefined && prior !== path) {\n      throw new Error(`case-fold repository path collision: ${prior} and ${path}`);\n    }\n    folded.set(key, path);\n  }\n}","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/codex/src/harness/repository-state.ts#L189-L225","documentation":"normalizeRelativePath() validates that a repository-relative path is safe before it is used by the source-state harness: it must be non-empty, not absolute, not dash-prefixed, and must contain no empty, '.', or '..' path segments. It exists to prevent path traversal and flag-lookalike arguments from entering git invocations and digest computations.","triggerScenarios":"Calling the harness with a path like '', '/etc/passwd', '-rf', '../outside/file', 'a//b', 'a/./b', or 'a/../b'. The check also fires on Windows backslash separators (a sibling check throws for those first) and rejects paths that are still absolute after NFC normalization.","commonSituations":"User- or agent-supplied filenames from CLI args or manifests contain absolute paths, leading './', or '..' segments; symlink targets are fed in unnormalized; paths built with path.join(repoRoot, file) are passed instead of the relative portion.","solutions":["Pass paths relative to the repository root, produced by path.relative(repoRoot, absolutePath)","Strip leading './' and reject absolute paths or '..' segments in your own input validation before calling the harness","If the string came from user input, normalize it (NFC) and validate segments with the same rules first"],"exampleFix":"// before\ncaptureEntry(path.join(repoRoot, userInput)); // '/repo/../etc/passwd' -> throws\n// after\nconst rel = path.relative(repoRoot, path.resolve(repoRoot, userInput));\nif (rel.startsWith('..') || path.isAbsolute(rel)) throw new TypeError('path outside repo');\ncaptureEntry(rel);","handlingStrategy":"validation","validationCode":"function isSafeRelativePath(p: string): boolean {\n  return typeof p === 'string'\n    && !p.includes('\\\\')\n    && p.length > 0\n    && !path.isAbsolute(p)\n    && !p.startsWith('-')\n    && !p.split('/').some(seg => seg === '' || seg === '.' || seg === '..');\n}","typeGuard":"function isSafeRelativePath(p: unknown): p is string {\n  return typeof p === 'string'\n    && p.length > 0 && !p.includes('\\\\')\n    && !path.isAbsolute(p) && !p.startsWith('-')\n    && !p.split('/').some(s => s === '' || s === '.' || s === '..');\n}","tryCatchPattern":"try { harness.capture(rel); } catch (e) { if (e instanceof Error && e.message.startsWith('unsafe repository-relative path')) throw new TypeError(`Invalid input path: ${JSON.stringify(userInput)}`); throw e; }","preventionTips":["Always derive relative paths via path.relative(repoRoot, absolute)","Treat user-supplied filenames as untrusted: validate segments before they reach any harness API","Never path.join an absolute path into a field documented as repository-relative"],"tags":["path-validation","security","git","harness"],"backgroundTag":"path-traversal-rejected","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-22T09:17:25.309Z"}