{"record":{"id":"ec6e552e3d277e24","repo":"abhigyanpatwari/GitNexus","slug":"filename-resolves-outside-the-repository-root","errorCode":null,"errorMessage":"${filename} resolves outside the repository root","messagePattern":"(.+?) resolves outside the repository root","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/config/repo-control-file.ts","lineNumber":15,"sourceCode":"import fs from 'node:fs';\nimport * as path from 'node:path';\n\nexport const MAX_REPO_CONTROL_FILE_BYTES = 1024 * 1024;\n\n/** Read a bounded, regular control file owned by the repository root. */\nexport async function readRepoControlFile(\n  repoRoot: string,\n  filename: string,\n): Promise<string | null> {\n  const requestedRoot = path.resolve(repoRoot);\n  const requested = path.resolve(requestedRoot, filename);\n  const relative = path.relative(requestedRoot, requested);\n  if (relative.startsWith('..') || path.isAbsolute(relative)) {\n    throw new Error(`${filename} resolves outside the repository root`);\n  }\n\n  try {\n    const canonicalRoot = fs.realpathSync(requestedRoot);\n    const beforeOpen = fs.lstatSync(requested);\n    if (beforeOpen.isSymbolicLink()) throw new Error(`${filename} must not be a symbolic link`);\n    if (!beforeOpen.isFile()) throw new Error(`${filename} must be a regular file`);\n    if (beforeOpen.nlink !== 1) throw new Error(`${filename} must not be a hard link`);\n    if (beforeOpen.size > MAX_REPO_CONTROL_FILE_BYTES) {\n      throw new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`);\n    }\n    return await new Promise<string>((resolve, reject) => {\n      const stream = fs.createReadStream(requested, {\n        flags: 'r',\n        start: 0,\n        end: MAX_REPO_CONTROL_FILE_BYTES,\n        autoClose: true,\n      });","sourceCodeStart":1,"sourceCodeEnd":33,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/config/repo-control-file.ts#L1-L33","documentation":"Thrown by readRepoControlFile in repo-control-file.ts when the requested filename, resolved against the repo root, escapes that root: path.relative() yields a path starting with '..' or an absolute path. Repo control files (like .gitnexusrc) are only read from inside the repository to prevent path traversal and reading files outside the project.","triggerScenarios":"Passing a filename such as '../other/repo/.gitnexusrc', '/etc/passwd', an absolute path outside the root, or a symlink-resolving name whose lexical resolution escapes the root to readRepoControlFile/loadAnalyzeConfigStrict.","commonSituations":"Building the config path with untrusted user input, misconfigured working directory so path.resolve lands outside the repo, test harnesses pointing at fixture files outside the repo root, or ../-relative path templates.","solutions":["Place the control file inside the repository root and reference it by a root-relative name, e.g. '.gitnexusrc'.","Remove any '../' or absolute-path components from the filename argument.","Ensure the process runs with the correct repo root so path.resolve(repoRoot, filename) stays inside it.","If you need config from another location, copy/symlink-check it into the repo (a plain in-repo file, since symlinks are rejected separately)."],"exampleFix":"// before\nawait readRepoControlFile(repoRoot, '../shared/.gitnexusrc');\n\n// after\ncp('../shared/.gitnexusrc', path.join(repoRoot, '.gitnexusrc'));\nawait readRepoControlFile(repoRoot, '.gitnexusrc');","handlingStrategy":"validation","validationCode":"import path from 'node:path';\nexport function assertInsideRepo(repoRoot: string, filename: string): void {\n  const root = path.resolve(repoRoot);\n  const rel = path.relative(root, path.resolve(root, filename));\n  if (rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new Error(`${filename} resolves outside the repository root`);\n  }\n}","typeGuard":null,"tryCatchPattern":"try {\n  const cfg = await readRepoControlFile(repoRoot, filename);\n} catch (e) {\n  if (e.message.includes('resolves outside the repository root')) {\n    console.error(`Control file must live inside ${repoRoot}; got ${filename}`);\n    process.exitCode = 1;\n  } else throw e;\n}","preventionTips":["Always pass root-relative filenames like '.gitnexusrc', never user-supplied absolute or ../ paths.","Sanitize any user input that flows into the filename argument (strip leading '/' and '..' segments).","Verify the process working directory / repoRoot detection so resolution stays in-repo."],"tags":["security","path-traversal","config","validation"],"backgroundTag":"path-traversal-blocked","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-09-01T13:15:02.810Z","contentChangedAt":"2026-09-01T13:15:02.810Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}