{"record":{"id":"9cd1057773da3bdc","repo":"abhigyanpatwari/GitNexus","slug":"clone-target-must-resolve-inside-root","errorCode":null,"errorMessage":"Clone target must resolve inside ${root}","messagePattern":"Clone target must resolve inside (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"gitnexus/src/server/git-clone.ts","lineNumber":502,"sourceCode":"\n  return safeTarget;\n}\n\nasync function assertPreRealpathContainment(root: string, target: string): Promise<void> {\n  const realRoot = await fs.realpath(root);\n  const realParent = await fs.realpath(path.dirname(target));\n  const parentRel = path.relative(realRoot, realParent);\n  if (parentRel.startsWith('..') || path.isAbsolute(parentRel)) {\n    throw new Error(`Clone target parent must resolve inside ${root}`);\n  }\n}\n\nasync function assertPostRealpathContainment(root: string, target: string): Promise<void> {\n  const realRoot = await fs.realpath(root);\n  const realTarget = await fs.realpath(target);\n  const rel = path.relative(realRoot, realTarget);\n  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new Error(`Clone target must resolve inside ${root}`);\n  }\n}\n\nasync function assertNoSymlinkPath(\n  root: string,\n  target: string,\n  verifyOwnership = false,\n): Promise<void> {\n  const resolvedRoot = path.resolve(root);\n  const resolvedTarget = path.resolve(target);\n  const relativeTarget = path.relative(resolvedRoot, resolvedTarget);\n  if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) return;\n  let current = resolvedRoot;\n  for (const segment of relativeTarget.split(path.sep).filter(Boolean)) {\n    current = path.join(current, segment);\n    let stat;\n    try {\n      stat = await fs.lstat(current);","sourceCodeStart":484,"sourceCodeEnd":520,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/0d1aed942f0e8b5d3bac27519fff441aceea722d/gitnexus/src/server/git-clone.ts#L484-L520","documentation":"assertPostRealpathContainment runs after clone/pull and requires the fully resolved target directory to be strictly inside the resolved clone root (empty relative path, '..', or absolute all fail). Unlike the parent-only pre-check, this catches targets that were swapped or symlinked during the operation (TOCTOU) and confirms the final checkout really landed under the root.","triggerScenarios":"The target itself becomes a symlink during the operation; a race swaps the directory between pre-check and post-check; targetDir resolves to the clone root itself; nested symlink introduced mid-clone.","commonSituations":"Concurrent processes manipulating the clone tree; malicious or buggy tooling replacing directories during long clones; verification-style setups running the function on paths that are later linked elsewhere.","solutions":["Ensure no process replaces or symlinks the target directory during the clone/pull.","Re-check the final path: it must resolve strictly under the clone root; recreate it as a real directory if it is a symlink.","Re-run the operation after removing any symlink at the target path.","Serialize access to the clone root (avoid concurrent clones to the same target)."],"exampleFix":"// before\nawait cloneOrPull({ url, targetDir: maybeSymlinkedPath });\n// after\nconst st = await fs.lstat(maybeSymlinkedPath);\nif (st.isSymbolicLink()) throw new Error('remove symlink first');\nawait cloneOrPull({ url, targetDir: maybeSymlinkedPath });","handlingStrategy":"validation","validationCode":"import { fs } from 'node:fs/promises';\nconst realRoot = await fs.realpath(root);\nlet realTarget = target;\ntry { realTarget = await fs.realpath(target); } catch { /* not created yet: OK pre-clone */ }\nif (!realTarget.startsWith(realRoot + path.sep)) {\n  throw new Error(`target resolves outside clone root`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await cloneOrPull(opts);\n} catch (err) {\n  if ((err as Error).message.startsWith('Clone target must resolve inside')) {\n    throw new Error(`Target at ${opts.targetDir} was swapped or symlinked during sync; inspect the directory.`, { cause: err });\n  }\n  throw err;\n}","preventionTips":["Don't run concurrent jobs that mutate or relink the clone tree.","Re-verify the final checkout path after any long-running clone.","Keep the clone root on a filesystem you control exclusively.","Re-run the operation after removing any symlink at the target path."],"tags":["git","security","symlink","race-condition"],"backgroundTag":"path-traversal-blocked","analyzedSha":"0d1aed942f0e8b5d3bac27519fff441aceea722d","analyzedAt":"2026-09-08T00:40:44.970Z","contentChangedAt":"2026-09-08T00:40:44.970Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}