abhigyanpatwari/GitNexus · error · ForbiddenError
Path traversal denied
Error message
Path traversal denied
What it means
The core anti-traversal guard of assertSafePath: after resolving root + relative path, the result must equal the root or sit under it (prefix check with a platform path separator). If path.resolve escapes the root — via ../ chains, or an absolute path that shadows the join — the request is refused with ForbiddenError (HTTP 403). It mirrors the guard historically at api.ts:1067-1077 and is deliberately applied before any filesystem call.
Source
Thrown at gitnexus/src/server/validation.ts:88
*
* Returns the absolute resolved path. Rejects empty paths, null bytes, and
* paths that resolve outside the root (e.g., `../../../etc/passwd`).
*
* @throws BadRequestError when the path is empty or contains a null byte
* @throws ForbiddenError when the resolved path escapes the root
*/
export function assertSafePath(rawPath: string, root: string): string {
if (rawPath.length === 0) {
throw new BadRequestError('Path must not be empty');
}
if (rawPath.includes('\0')) {
throw new BadRequestError('Path must not contain null bytes');
}
const resolvedRoot = path.resolve(root);
const fullPath = path.resolve(resolvedRoot, rawPath);
const safePrefix = resolvedRoot.endsWith(path.sep) ? resolvedRoot : resolvedRoot + path.sep;
if (fullPath !== resolvedRoot && !fullPath.startsWith(safePrefix)) {
throw new ForbiddenError('Path traversal denied');
}
return fullPath;
}
/**
* Escape regex metacharacters in a user-supplied string so it can be safely
* embedded as a literal in `new RegExp(...)`. Used by /api/grep's literal mode
* and any future endpoint that constructs a regex from caller input.
*/
export function escapeRegExp(input: string): string {
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
/**
* Default rate-limit policy for FS-touching API routes (CodeQL
* js/missing-rate-limiting). Tuned for the local-bound HTTP server's expected
* traffic — interactive web UI use stays well under the limit; abusive loops
* trip 429.View on GitHub (pinned to aac7515d2a)
Solutions
- Send paths relative to the endpoint's documented root (the repo/index root)
- If only a filename is meaningful, send just the basename and let the server join it
- Server-side, keep using assertSafePath (or path.basename) ahead of every fs call — never rely on client promises
Example fix
// before
api.readFile({ path: '/home/me/repo/src/a.ts' }); // absolute → 403
// after
api.readFile({ path: 'src/a.ts' }); // relative to the repo root Defensive patterns
Strategy: validation
Validate before calling
import path from 'node:path';
if (path.isAbsolute(rel) || rel.split(/[\\/]+/).includes('..')) {
throw new Error('send a path relative to the repo root without ".." segments');
} Prevention
- Always send repo-relative paths, never absolute local paths
- When only a filename matters, send the basename and let the server join it
- Never bypass or catch-and-ignore the 403 — it means the path would escape the allowed root
When it happens
Trigger: ?path=../../../etc/passwd, ?path=/etc/passwd (an absolute path resolves outside the allowed root), or Windows drive-relative forms like ?path=..\..\c:\windows.
Common situations: Clients sending absolute local paths where a repo-relative path is expected; UI file trees passing the full local path instead of the displayed relative one; deliberate traversal probes.
Related errors
- Path traversal denied
- Refusing to delete ${dbPath}: resolved path ${realPath} is o
- Path traversal blocked: ${filePath}
- Clone target must be a subdirectory of ${CLONE_ROOT}
- Path must not contain null bytes
AI-assisted analysis of abhigyanpatwari/GitNexus@aac7515d2a (2026-08-20).
Data as JSON: /api/errors/423c811c019dda69.
Report an issue: GitHub.