{"record":{"id":"65f673f9d2542e61","repo":"abhigyanpatwari/GitNexus","slug":"path-traversal-blocked-filepath","errorCode":null,"errorMessage":"Path traversal blocked: ${filePath}","messagePattern":"Path traversal blocked: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/mcp/local/local-backend.ts","lineNumber":5585,"sourceCode":"    },\n  ): Promise<any> {\n    await this.ensureInitialized(repo);\n\n    const { new_name, file_path } = params;\n    const dry_run = params.dry_run ?? true;\n\n    if (!params.symbol_name && !params.symbol_uid) {\n      return { error: 'Either symbol_name or symbol_uid is required.' };\n    }\n\n    /** Guard: ensure a file path resolves within the repo root (prevents path traversal) */\n    const assertSafePath = (filePath: string): string => {\n      const full = path.resolve(repo.repoPath, filePath);\n      const safePrefix = repo.repoPath.endsWith(path.sep)\n        ? repo.repoPath\n        : repo.repoPath + path.sep;\n      if (!full.startsWith(safePrefix) && full !== repo.repoPath) {\n        throw new Error(`Path traversal blocked: ${filePath}`);\n      }\n      return full;\n    };\n\n    // Step 1: Find the target symbol (reuse context's lookup)\n    const lookupResult = await this.context(repo, {\n      name: params.symbol_name,\n      uid: params.symbol_uid,\n      file_path,\n    });\n\n    if (lookupResult.status === 'ambiguous') {\n      return lookupResult; // pass disambiguation through\n    }\n    if (lookupResult.error) {\n      return lookupResult;\n    }\n","sourceCodeStart":5567,"sourceCodeEnd":5603,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/mcp/local/local-backend.ts#L5567-L5603","documentation":"Inside the rename tool, assertSafePath guards every file_path: it resolves the path against repo.repoPath and requires the result to share the repo root prefix (or equal it). Any input that escapes the root — ../ sequences, absolute paths outside the repo, or paths whose resolved form lands elsewhere — is rejected before any file is touched. It is a path-traversal security guard for a write-capable tool.","triggerScenarios":"Calling the rename tool with file_path like '../../other-project/src/foo.ts', an absolute path that is not under repo.repoPath, or a path built from client-side absolute locations that differ from the indexed repoPath (repo moved/re-cloned since indexing).","commonSituations":"Clients forwarding absolute editor paths after the repo was moved to a new directory (prefix mismatch, not an attack); agents constructing paths from cwd of a different checkout; genuinely hostile input when the MCP server is exposed beyond loopback; symlinked workspaces resolving outside the root.","solutions":["Pass repo-relative paths (e.g. 'src/core/foo.ts') instead of absolute or ../ paths.","If the repo moved on disk, re-run `gitnexus analyze` from the new location so repoPath matches reality, then retry.","When you must pass a path, derive it by stripping the repo root prefix client-side.","Do not attempt to bypass the guard — it intentionally blocks writes outside the indexed repo."],"exampleFix":"# before: absolute path outside the indexed repoPath\n{\"tool\": \"rename\", \"args\": {\"symbol_name\": \"parseRepo\", \"new_name\": \"resolveRepo\",\n  \"file_path\": \"/home/me/other-checkout/src/backend.ts\", \"dry_run\": true}}\n# → Path traversal blocked: /home/me/other-checkout/src/backend.ts\n\n# after: repo-relative path under the indexed root\n{\"tool\": \"rename\", \"args\": {\"symbol_name\": \"parseRepo\", \"new_name\": \"resolveRepo\",\n  \"file_path\": \"src/backend.ts\", \"dry_run\": true}}","handlingStrategy":"validation","validationCode":"// Normalize to a repo-relative POSIX path before calling rename\nimport { relative, isAbsolute, resolve } from 'node:path';\n\nfunction toRepoRelative(repoRoot: string, filePath: string): string {\n  const abs = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);\n  const rel = relative(repoRoot, abs);\n  if (rel.startsWith('..') || isAbsolute(rel)) {\n    throw new Error(`file_path escapes repo root \"${repoRoot}\": ${filePath}`);\n  }\n  return rel.split('\\\\').join('/');\n}","typeGuard":"const isSafeRepoPath = (repoRoot: string, filePath: string): boolean => {\n  const full = resolve(repoRoot, filePath);\n  const prefix = repoRoot.endsWith('/') ? repoRoot : repoRoot + '/';\n  return full === repoRoot || full.startsWith(prefix);\n}; // mirrors the server-side assertSafePath contract","tryCatchPattern":"try {\n  return await client.callTool({ name: 'rename', arguments: params });\n} catch (err) {\n  if (err instanceof Error && err.message.startsWith('Path traversal blocked')) {\n    // client bug or stale repoPath: never bypass — rebase the path onto the indexed root\n    const rel = toRepoRelative(indexedRoot, params.file_path);\n    return client.callTool({ name: 'rename', arguments: { ...params, file_path: rel } });\n  }\n  throw err;\n}","preventionTips":["Send repo-relative POSIX paths in all file_path parameters; never absolute paths.","Re-run `gitnexus analyze` after moving a repo so the registry's repoPath matches disk.","Validate paths with a resolve-and-prefix check client-side, mirroring assertSafePath.","Treat this error as a prompt-response signal: it fired before any file was touched, so nothing was modified."],"tags":["mcp","rename","path-traversal","security","input-validation","file-path"],"backgroundTag":"path-traversal-blocked","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T20:17:22.307Z"}