{"record":{"id":"879a0aebb96d4abf","repo":"abhigyanpatwari/GitNexus","slug":"path-traversal-denied-879a0a","errorCode":null,"errorMessage":"Path traversal denied","messagePattern":"Path traversal denied","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"gitnexus/src/server/api.ts","lineNumber":604,"sourceCode":"    // parameter-tampering, same class as the /api/grep critical fix).\n    const rawFilePath = req.query.path;\n    if (rawFilePath === undefined || rawFilePath === '') {\n      res.status(400).json({ error: 'Missing path' });\n      return;\n    }\n    const filePath = assertString(rawFilePath, 'path');\n\n    // Path-injection containment — inline at the sink with the canonical\n    // path.relative idiom that CodeQL's js/path-injection sanitizer\n    // recognizes. assertSafePath in validation.ts performs the equivalent\n    // check, but cross-module helpers are not followed by CodeQL's\n    // interprocedural analysis for path-traversal sanitization in JS, so\n    // the barrier must be visible inline at the readFile sink.\n    const repoRoot = path.resolve(repoPath);\n    const fullPath = path.resolve(repoRoot, filePath);\n    const fullRel = path.relative(repoRoot, fullPath);\n    if (fullRel.startsWith('..') || path.isAbsolute(fullRel)) {\n      res.status(403).json({ error: 'Path traversal denied' });\n      return;\n    }\n\n    const raw = await fs.readFile(fullPath, 'utf-8');\n\n    // Optional line-range support: ?startLine=10&endLine=50\n    // Returns only the requested slice (0-indexed), plus metadata.\n    const startLine = req.query.startLine !== undefined ? Number(req.query.startLine) : undefined;\n    const endLine = req.query.endLine !== undefined ? Number(req.query.endLine) : undefined;\n\n    if (startLine !== undefined && Number.isFinite(startLine)) {\n      const lines = raw.split('\\n');\n      const start = Math.max(0, startLine);\n      const end =\n        endLine !== undefined && Number.isFinite(endLine)\n          ? Math.min(lines.length, endLine + 1)\n          : lines.length;\n      res.json({","sourceCodeStart":586,"sourceCodeEnd":622,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/aac7515d2a8c50a1f8f923c6fb77218b333560d6/gitnexus/src/server/api.ts#L586-L622","documentation":"HTTP 403 from the repo file-read API when the requested path resolves outside the repository root. The check is the canonical CodeQL-recognized sanitizer: it resolves repoRoot and the joined path, computes path.relative(repoRoot, fullPath), and denies when the result starts with '..' or is absolute. This blocks both traversal (../ chains) and absolute-path injection (leading / or Windows drive letters) before the fs.readFile sink.","triggerScenarios":"GET ?path=../../etc/passwd or any ..-chain escaping the repo; ?path=/etc/passwd (absolute path: path.relative yields an absolute-ish result on other roots); Windows-shaped input like ?path=C:\\Windows\\system.ini; encoded traversal (%2e%2e%2f) which express decodes before the check, so it is caught the same way.","commonSituations":"Frontends concatenating a user-typed absolute path onto the query; file trees containing symlinks whose textual representation is stored as an absolute path; security scanners (CodeQL, Burp) probing the endpoint — this 403 is the expected, correct response; client code accidentally sending a leading slash for repo-root files (?path=/README.md).","solutions":["Send paths relative to the repo root without a leading slash: ?path=README.md, ?path=src/index.ts","Strip leading slashes and reject '..' segments client-side before building the URL","If you genuinely need a file outside the repo, re-index that location as its own repository instead of traversing","Treat this 403 in security tests as a pass — the containment is working; fix the caller, not the server"],"exampleFix":"// before\nfetch(`${base}/api/file?path=${absolutePathOnDisk}`); // 403\n\n// after\nconst rel = path.relative(repoRoot, absolutePathOnDisk); // normalize first\nif (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error('outside repo');\nfetch(`${base}/api/file?path=${encodeURIComponent(rel)}`);","handlingStrategy":"validation","validationCode":"// Normalize to a repo-relative path and reject escapes before the request.\nfunction toRepoRelative(repoRoot: string, p: string): string {\n  const rel = path.relative(path.resolve(repoRoot), path.resolve(repoRoot, p));\n  if (rel.startsWith('..') || path.isAbsolute(rel)) throw new Error(`path escapes repo root: ${p}`);\n  return rel;\n}","typeGuard":null,"tryCatchPattern":"const res = await fetch(url);\nif (res.status === 403) {\n  const { error } = await res.json();\n  if (error === 'Path traversal denied') throw new TypeError(`path must be repo-relative: ${requestedPath}`);\n}","preventionTips":["Always request repo-relative paths without leading slashes","Strip '..' segments client-side before building URLs","Never send absolute filesystem paths to the file API","In security tests, treat this 403 as the pass condition"],"tags":["security","path-traversal","http-403","api"],"backgroundTag":"path-traversal-blocked","analyzedSha":"aac7515d2a8c50a1f8f923c6fb77218b333560d6","analyzedAt":"2026-08-20T23:29:22.980Z","schemaVersion":2},"datasetVersion":"2026-08-22T20:17:22.307Z"}