abhigyanpatwari/GitNexus · error
Path traversal blocked: ${filePath}
Error message
Path traversal blocked: ${filePath} What it means
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.
Source
Thrown at gitnexus/src/mcp/local/local-backend.ts:5585
},
): Promise<any> {
await this.ensureInitialized(repo);
const { new_name, file_path } = params;
const dry_run = params.dry_run ?? true;
if (!params.symbol_name && !params.symbol_uid) {
return { error: 'Either symbol_name or symbol_uid is required.' };
}
/** Guard: ensure a file path resolves within the repo root (prevents path traversal) */
const assertSafePath = (filePath: string): string => {
const full = path.resolve(repo.repoPath, filePath);
const safePrefix = repo.repoPath.endsWith(path.sep)
? repo.repoPath
: repo.repoPath + path.sep;
if (!full.startsWith(safePrefix) && full !== repo.repoPath) {
throw new Error(`Path traversal blocked: ${filePath}`);
}
return full;
};
// Step 1: Find the target symbol (reuse context's lookup)
const lookupResult = await this.context(repo, {
name: params.symbol_name,
uid: params.symbol_uid,
file_path,
});
if (lookupResult.status === 'ambiguous') {
return lookupResult; // pass disambiguation through
}
if (lookupResult.error) {
return lookupResult;
}
View on GitHub (pinned to aac7515d2a)
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.
Example fix
# before: absolute path outside the indexed repoPath
{"tool": "rename", "args": {"symbol_name": "parseRepo", "new_name": "resolveRepo",
"file_path": "/home/me/other-checkout/src/backend.ts", "dry_run": true}}
# → Path traversal blocked: /home/me/other-checkout/src/backend.ts
# after: repo-relative path under the indexed root
{"tool": "rename", "args": {"symbol_name": "parseRepo", "new_name": "resolveRepo",
"file_path": "src/backend.ts", "dry_run": true}} Defensive patterns
Strategy: validation
Validate before calling
// Normalize to a repo-relative POSIX path before calling rename
import { relative, isAbsolute, resolve } from 'node:path';
function toRepoRelative(repoRoot: string, filePath: string): string {
const abs = isAbsolute(filePath) ? filePath : resolve(process.cwd(), filePath);
const rel = relative(repoRoot, abs);
if (rel.startsWith('..') || isAbsolute(rel)) {
throw new Error(`file_path escapes repo root "${repoRoot}": ${filePath}`);
}
return rel.split('\\').join('/');
} Type guard
const isSafeRepoPath = (repoRoot: string, filePath: string): boolean => {
const full = resolve(repoRoot, filePath);
const prefix = repoRoot.endsWith('/') ? repoRoot : repoRoot + '/';
return full === repoRoot || full.startsWith(prefix);
}; // mirrors the server-side assertSafePath contract Try / catch
try {
return await client.callTool({ name: 'rename', arguments: params });
} catch (err) {
if (err instanceof Error && err.message.startsWith('Path traversal blocked')) {
// client bug or stale repoPath: never bypass — rebase the path onto the indexed root
const rel = toRepoRelative(indexedRoot, params.file_path);
return client.callTool({ name: 'rename', arguments: { ...params, file_path: rel } });
}
throw err;
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
Related errors
- Refusing to start the MCP HTTP server on a non-loopback host
- list_repos: "${field}" must be an integer ${bound} (received
- Path must not contain null bytes
- Path traversal denied
- Path traversal denied
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/65f673f9d2542e61.
Report an issue: GitHub.