{"record":{"id":"fecac40072d19ad6","repo":"abhigyanpatwari/GitNexus","slug":"filename-must-not-be-a-symbolic-link","errorCode":null,"errorMessage":"${filename} must not be a symbolic link","messagePattern":"(.+?) must not be a symbolic link","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/config/repo-control-file.ts","lineNumber":21,"sourceCode":"\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      });\n      const chunks: Buffer[] = [];\n      let totalBytes = 0;\n      let validated = false;\n      let settled = false;\n\n      const finish = (value: string): void => {","sourceCodeStart":3,"sourceCodeEnd":39,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/52924ef12c2290ceee4612526a828ec4cdf2047f/gitnexus/src/config/repo-control-file.ts#L3-L39","documentation":"Thrown by readRepoControlFile in repo-control-file.ts after an lstat shows the requested control file is a symbolic link. Repo control files must be plain, first-party files inside the repo; symlinks could redirect reads to attacker-controlled or out-of-repo targets, so they are rejected outright as a TOCTOU-hardening measure.","triggerScenarios":"Creating `ln -s ~/.gitnexusrc-global .gitnexusrc` in the repo, symlinked fixture files in a test checkout, package managers or dotfile managers (stow, chezmoi) that replace config files with symlinks, then running analyze with strict config loading.","commonSituations":"Dotfile management setups symlinking .gitnexusrc from a home repo; sharing one config across repos via symlink; build/test scripts linking fixture configs into the repo.","solutions":["Replace the symlink with a real file: `rm .gitnexusrc && cp /path/to/target .gitnexusrc`.","If a dotfile manager created the link, configure it to copy instead of symlink for this path.","Keep per-repo copies of the control file instead of sharing one via symlinks.","Update test fixtures to copy files into the repo rather than linking them."],"exampleFix":"// before\nln -s ~/dotfiles/.gitnexusrc .gitnexusrc\n\n// after\nrm .gitnexusrc\ncp ~/dotfiles/.gitnexusrc .gitnexusrc","handlingStrategy":"validation","validationCode":"import fs from 'node:fs';\nimport path from 'node:path';\nexport function assertRegularFile(repoRoot: string, filename: string): void {\n  const st = fs.lstatSync(path.resolve(repoRoot, filename));\n  if (st.isSymbolicLink()) throw new Error(`${filename} must not be a symbolic link; replace with a real file`);\n  if (!st.isFile()) throw new Error(`${filename} must be a regular file`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  const cfg = await readRepoControlFile(repoRoot, filename);\n} catch (e) {\n  if (e.message.includes('must not be a symbolic link')) {\n    console.error(`Replace symlink ${filename} with a real file (rm + cp).`);\n    process.exitCode = 1;\n  } else throw e;\n}","preventionTips":["Configure dotfile managers (stow, chezmoi) to copy rather than symlink .gitnexusrc.","Check `ls -l` for the control file after cloning or provisioning a repo.","Use per-repo copies of control files instead of a shared symlinked config."],"tags":["security","symlink","config","file-validation"],"backgroundTag":"symlink-rejected","analyzedSha":"52924ef12c2290ceee4612526a828ec4cdf2047f","analyzedAt":"2026-09-01T13:15:02.810Z","contentChangedAt":"2026-09-01T13:15:02.810Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}