abhigyanpatwari/GitNexus · error · Error
Refusing symlink in clone target path: ${current}
Error message
Refusing symlink in clone target path: ${current} What it means
assertNoSymlinkPath walks every path component from the clone root down to the target, lstat-ing each; if any component is a symbolic link the operation is refused. Unlike the realpath containment checks (which allow symlinks that still land inside the root), this is a stricter zero-symlink policy on the clone target path, optionally verifying ownership/permissions of each component.
Source
Thrown at gitnexus/src/server/git-clone.ts:526
target: string,
verifyOwnership = false,
): Promise<void> {
const resolvedRoot = path.resolve(root);
const resolvedTarget = path.resolve(target);
const relativeTarget = path.relative(resolvedRoot, resolvedTarget);
if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) return;
let current = resolvedRoot;
for (const segment of relativeTarget.split(path.sep).filter(Boolean)) {
current = path.join(current, segment);
let stat;
try {
stat = await fs.lstat(current);
} catch (err: unknown) {
if ((err as NodeJS.ErrnoException).code === 'ENOENT') break;
throw err;
}
if (stat.isSymbolicLink()) {
throw new Error(`Refusing symlink in clone target path: ${current}`);
}
if (verifyOwnership) await assertDirectoryOwnerAndPermissions(current);
}
}
/**
* Hosts the per-request GitHub PAT may be sent to. Exported so the
* /api/analyze boundary check and this injection-site check share one
* allowlist (they must agree, or a token accepted by the API could be
* silently dropped — or worse — at injection).
*/
export const GITHUB_TOKEN_HOSTS: ReadonlySet<string> = new Set(['github.com', 'www.github.com']);
/**
* Resolve at most ONE git credential for a clone/pull, by server-side policy
* keyed on the clone host against a fixed allowlist (never a free-form user
* toggle):
* 1. a per-request GitHub PAT — only for hosts in GITHUB_TOKEN_HOSTS;View on GitHub (pinned to 0d1aed942f)
Solutions
- Replace the symlink component with a real directory (or bind-mount the volume instead of symlinking).
- Point targetDir at a path with no symlinked components under the clone root.
- Move the clone root itself (allowedCloneRoot) to the real filesystem location instead of linking to it.
- Check each component with fs.lstat before calling to find which one is the link.
Example fix
// before ln -s /mnt/bigdisk/clones /srv/clones // symlink triggers the error // after mount --bind /mnt/bigdisk/clones /srv/clones // real dir, no symlink in path
Defensive patterns
Strategy: validation
Validate before calling
import { fs } from 'node:fs/promises';
async function hasSymlinkComponent(root: string, target: string): Promise<boolean> {
let cur = path.resolve(target);
const stop = path.resolve(root);
while (cur !== stop && cur.startsWith(stop)) {
const st = await fs.lstat(cur);
if (st.isSymbolicLink()) return true;
cur = path.dirname(cur);
}
return false;
} Try / catch
try {
await cloneOrPull(opts);
} catch (err) {
if ((err as Error).message.startsWith('Refusing symlink in clone target path')) {
const m = (err as Error).message.match(/path: (.+)$/);
throw new Error(`Replace symlink ${m?.[1]} with a real directory or bind-mount.`, { cause: err });
}
throw err;
} Prevention
- Use bind mounts instead of symlinks for relocating clone storage.
- Audit the clone root tree for symlinks (find root -type l) on a schedule.
- Keep per-repo clones directly under the real root with no intermediate links.
- On shared machines, treat any symlink in the clone path as a potential repo-swap attack.
When it happens
Trigger: Any directory in the path between the clone root and targetDir is a symlink (e.g. clones -> /mnt/bigdisk/clones); an attacker pre-plants a symlink component so a future clone writes through it; a user reorganized directories with links after an earlier clone.
Common situations: Users symlink clone roots to bigger disks; per-project symlinked scratch dirs; shared machines where symlinks are an attack vector for repo-swap attacks; container images that link /var/git to another volume.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Clone target parent must resolve inside ${root}
- Clone target must resolve inside ${root}
- ${TRUSTED_CACHE_DIRECTORY_ENV} must not traverse symbolic li
- ${filename} must not be a symbolic link
- path must include owner/repo without traversal
AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08).
Data as JSON: /api/errors/b3b9c314262514cd.
Report an issue: GitHub.