{"record":{"id":"aa7400efb4aa8a30","repo":"stablyai/orca","slug":"clone-path-must-be-inside-the-destination-director","errorCode":null,"errorMessage":"Clone path must be inside the destination directory","messagePattern":"Clone path must be inside the destination directory","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/main/git/repo-clone-path.ts","lineNumber":53,"sourceCode":"    !isAbsolute(args.destination) ||\n    (process.platform !== 'win32' && isWindowsAbsolutePathLike(args.destination))\n  ) {\n    throw new Error('Clone destination must be an absolute path')\n  }\n\n  const repoName = deriveCloneRepoNameFromUrl(args.url)\n\n  const clonePath = join(args.destination, repoName)\n  const resolvedDestination = resolve(args.destination)\n  const resolvedClonePath = resolve(clonePath)\n  const pathFromDestination = relative(resolvedDestination, resolvedClonePath)\n  if (\n    pathFromDestination === '' ||\n    pathFromDestination === '..' ||\n    pathFromDestination.startsWith(`..${sep}`) ||\n    isAbsolute(pathFromDestination)\n  ) {\n    throw new Error('Clone path must be inside the destination directory')\n  }\n\n  return clonePath\n}\n\nexport function getClonePathComparisonKey(clonePath: string): string {\n  const resolvedClonePath = isWindowsAbsolutePathLike(clonePath) ? clonePath : resolve(clonePath)\n  const normalized = normalizeRuntimePathSeparators(resolvedClonePath)\n  const wslUncMatch = normalized.match(/^\\/\\/(?:wsl\\.localhost|wsl\\$)\\/([^/]+)(\\/.*)?$/i)\n  if (wslUncMatch) {\n    // Why: WSL UNC paths cross into a case-sensitive Linux filesystem, so only\n    // the Windows UNC server alias and distro segment should be case-folded.\n    const linuxPath = (wslUncMatch[2] ?? '').replace(/\\/+$/, '')\n    return `//wsl/${wslUncMatch[1].toLowerCase()}${linuxPath}`\n  }\n  return normalizeRuntimePathForComparison(resolvedClonePath)\n}\n","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/stablyai/orca/blob/1136503c6a231a16dce8f921f6fadb63d181e8db/src/main/git/repo-clone-path.ts#L35-L71","documentation":"deriveValidatedClonePath joins the destination with the derived repo name, resolves both, computes the relative path from the destination to the resolved clone path, and throws if that relative path escapes the destination (is '', '..', starts with '..<sep>', or is absolute). This is a security guard: a crafted URL whose repo name resolves outside the destination must not produce a clone path that a later rm could walk outside the intended folder.","triggerScenarios":"A URL whose derived repo name, after join+resolve, ends up outside args.destination — e.g. a destination that is itself a symlinked directory whose target resolves elsewhere, an adversarial URL that survives the :24/:27 guards but still moves the resolved path, or a junction/MSYS path on Windows whose resolution crosses drive roots.","commonSituations":"Cloning into a destination that is a symlink to another volume (resolve() follows symlinks, so the relative path can become absolute or '..'-prefixed); Windows MSYS path rewriting mangling the destination before it reaches the main process; adversarial automation passing crafted URLs to delete files outside the clone folder; a config-supplied destination that resolved differently at write time vs read time.","solutions":["Use a real, non-symlinked directory as the clone destination; symlinked destinations can resolve outside themselves.","Do not attempt to 'fix' the URL to dodge this guard — it is the last line preventing filesystem escape; fix the destination instead.","On Windows, pass native Windows paths and avoid MSYS/Git Bash path rewriting by using forward-slash absolute paths from the main process.","Run the clone under a dedicated, freshly-created base directory so resolution cannot cross into user data."],"exampleFix":"// before\nderiveValidatedClonePath({ url, destination: symlinkedDir })\n\n// after: resolve symlinks first, use a concrete directory\nimport { realpath } from 'node:fs/promises'\nconst realDest = await realpath(baseCloneDir)\nderiveValidatedClonePath({ url, destination: realDest })","handlingStrategy":"validation","validationCode":"import { realpath } from 'node:fs/promises'\nimport { relative, resolve, sep, isAbsolute } from 'node:path'\n\nasync function clonePathStaysInsideDestination(url: string, destination: string): Promise<boolean> {\n  const realDest = await realpath(destination)\n  const repoName = deriveCloneRepoNameFromUrl(url) // assume :24/:27 already pass\n  const resolvedClone = resolve(realDest, repoName)\n  const rel = relative(realDest, resolvedClone)\n  return rel !== '' && rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel)\n}","typeGuard":"function isClonePathOutsideDestination(error: unknown): boolean {\n  return error instanceof Error && error.message === 'Clone path must be inside the destination directory'\n}","tryCatchPattern":"const realDest = await realpath(destination)\nif (!(await clonePathStaysInsideDestination(url, realDest))) {\n  throw new Error('Refusing to clone: resolved path escapes the destination. Use a non-symlinked folder.')\n}\ntry { return deriveValidatedClonePath({ url, destination: realDest }) }\ncatch (error) { if (isClonePathOutsideDestination(error)) throw new Error('Destination resolves outside itself; pick a concrete folder.'); else throw error }","preventionTips":["Use a real, non-symlinked directory as the clone destination; realpath() it first.","Do not attempt to craft URLs that dodge this guard — it is the last line preventing filesystem escape.","On Windows, pass native Windows paths and avoid MSYS path rewriting."],"tags":["git","clone","security","path-traversal","filesystem"],"backgroundTag":null,"analyzedSha":"1136503c6a231a16dce8f921f6fadb63d181e8db","analyzedAt":"2026-08-12T23:15:58.167Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}