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

  1. Replace the symlink component with a real directory (or bind-mount the volume instead of symlinking).
  2. Point targetDir at a path with no symlinked components under the clone root.
  3. Move the clone root itself (allowedCloneRoot) to the real filesystem location instead of linking to it.
  4. 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

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


AI-assisted analysis of abhigyanpatwari/GitNexus@0d1aed942f (2026-09-08). Data as JSON: /api/errors/b3b9c314262514cd. Report an issue: GitHub.