{"record":{"id":"aa151ba35bf6fb4e","repo":"abhigyanpatwari/GitNexus","slug":"clone-target-must-be-a-subdirectory-of-cloneroot","errorCode":null,"errorMessage":"Clone target must be a subdirectory of ${cloneRoot}","messagePattern":"Clone target must be a subdirectory of (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"gitnexus/src/server/git-clone.ts","lineNumber":538,"sourceCode":"  // subprocess sink. The same `safeTarget` is used for every downstream\n  // path operation — no reassignment that the analyzer could lose track of.\n  //\n  // The lexical check runs before filesystem creation; realpath and symlink\n  // checks below run before pull/clone and again after clone completes.\n  const cloneRoot = path.resolve(options?.allowedCloneRoot ?? CLONE_ROOT);\n  const expectedRepoName = options?.expectedRepoName;\n  if (expectedRepoName !== undefined && expectedRepoName !== extractRepoName(url)) {\n    throw new Error(`Clone target repo name ${expectedRepoName} does not match requested URL`);\n  }\n\n  const safeTarget = path.resolve(targetDir);\n  if (expectedRepoName !== undefined && path.basename(safeTarget) !== expectedRepoName) {\n    throw new Error(`Clone target basename must match repository name ${expectedRepoName}`);\n  }\n\n  const rel = path.relative(cloneRoot, safeTarget);\n  if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {\n    throw new Error(`Clone target must be a subdirectory of ${cloneRoot}`);\n  }\n\n  // Always validate the requested URL — the prior shape only ran this in\n  // the code path where the repo was cloned. Now it runs unconditionally,\n  // preventing SSRF / blocked-host bypasses even when targetDir already exists.\n  if (options?.allowAutoSyncSsh) validateAutoSyncRemoteUrl(url);\n  else validateGitUrl(url);\n  await fs.mkdir(cloneRoot, { recursive: true });\n  if (options?.allowedCloneRoot) {\n    await assertDirectoryOwnerAndPermissions(cloneRoot);\n  }\n  await assertNoSymlinkPath(cloneRoot, safeTarget, Boolean(options?.allowedCloneRoot));\n  await fs.mkdir(path.dirname(safeTarget), { recursive: true });\n  await assertNoSymlinkPath(cloneRoot, safeTarget, Boolean(options?.allowedCloneRoot));\n  await assertPreRealpathContainment(cloneRoot, safeTarget);\n\n  const exists = await fs.access(path.join(safeTarget, '.git')).then(\n    () => true,","sourceCodeStart":520,"sourceCodeEnd":556,"githubUrl":"https://github.com/abhigyanpatwari/GitNexus/blob/ac9a4e9abd8fd3058c070b72c23402a4f887929a/gitnexus/src/server/git-clone.ts#L520-L556","documentation":"cloneOrPull throws this when the resolved targetDir is not strictly inside the allowed clone root: path.relative(cloneRoot, safeTarget) is empty (target IS the root), starts with '..' (escapes the root), or is absolute (different tree). This is a path-containment guard preventing clones from being written outside the sanctioned directory.","triggerScenarios":"Passing targetDir outside CLONE_ROOT or outside options.allowedCloneRoot; passing targetDir equal to the clone root itself; passing an absolute path into a different tree; path tricks where the resolved target escapes via '..'.","commonSituations":"Users configuring a custom clone destination outside the allowed root; tests using tmp dirs without setting allowedCloneRoot; callers joining targetDir from untrusted input that contains '..' segments.","solutions":["Move targetDir under the allowed clone root (or CLONE_ROOT), e.g. path.join(cloneRoot, repoName).","Set options.allowedCloneRoot to the intended parent directory of your target.","Ensure the target is a subdirectory, not the root itself — create a per-repo subdirectory.","Sanitize user-supplied target paths and resolve them before calling to guarantee containment."],"exampleFix":"// before\nawait cloneOrPull({ url, targetDir: '/srv/other/location/repo' });\n// after\nawait cloneOrPull({ url, targetDir: '/srv/clones/repo', allowedCloneRoot: '/srv/clones' });","handlingStrategy":"validation","validationCode":"import path from 'node:path';\nconst root = path.resolve(allowedCloneRoot ?? CLONE_ROOT);\nconst target = path.resolve(userSuppliedTarget);\nconst rel = path.relative(root, target);\nif (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {\n  throw new Error(`target must be inside ${root}`);\n}","typeGuard":null,"tryCatchPattern":"try {\n  await cloneOrPull(opts);\n} catch (err) {\n  if ((err as Error).message.startsWith('Clone target must be a subdirectory')) {\n    throw new Error(`Rejected clone destination outside allowed root ${opts.allowedCloneRoot ?? 'default'}`);\n  }\n  throw err;\n}","preventionTips":["Never pass raw user input as targetDir; resolve and join it against the clone root.","Set allowedCloneRoot explicitly in tests and production config.","Treat any '..' or absolute-path input as a security signal, not a path detail."],"tags":["git","security","path-traversal"],"backgroundTag":"path-traversal-blocked","analyzedSha":"ac9a4e9abd8fd3058c070b72c23402a4f887929a","analyzedAt":"2026-09-08T00:40:44.970Z","contentChangedAt":"2026-09-08T00:40:44.970Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}