Vulnerability GHSA-9qw7-j9xw-fv9c
Summary
isomorphic-git: Arbitrary file write outside of working tree via directory symlink during checkout (potential RCE)
Details
Summary
git.checkout writes working-tree files through a directory symlink without checking the leading path, so a malicious repository can write outside the intended working directory. The defect is in isomorphic-git's checkout logic itself and is therefore independent of the fs provider. The impact depends on the configured filesystem; worst case is arbitrary file write on the host, e.g. injecting an attacker key into ~/.ssh/authorized_keys → remote login / code execution.
Details
A single git tree can't hold a name as both a symlink and a directory, so the attack uses two states:
Commit N: config (mode 120000, symlink → ../.ssh)
Commit N+1: config/ (mode 040000, tree)
authorized_keys (mode 100644, attacker public key)
Checking out commit N writes config as a directory symlink. Checking out commit N+1 sees config/authorized_keys as a new path: isomorphic-git mkdirs the parent (config, already a symlink → EEXIST, ignored) then writeFiles the file, which the OS resolves through the symlink to outside the working directory. A file-level symlink swap is caught (the 120000→100644 mode change triggers an rm), but the directory-level variant is not — the payload is always a new child path, a plain create, so the parent symlink is never removed.
The symlink target is attacker-chosen. Besides ~/.ssh, pointing it at the repo's own control directory — hooks → .git/hooks then hooks/pre-commit, or x → .git then overwriting .git/config (core.hooksPath / alias / core.fsmonitor) — yields RCE on the next native git command without depending on ~/.ssh existing, since .git/ is always present in a fresh clone.
Canonical git behavior (divergence)
git refuses to do this: has_symlink_leading_path() (symlinks.c) checks each leading path component during checkout and will not follow a symlink — the hardening behind CVE-2021-21300 and CVE-2024-32002. isomorphic-git has no equivalent check.
PoC
https://github.com/spiceoverflow/av1-symlink-hook-rce — open in a Codespace; it builds the malicious repo, serves it locally, and runs a victim. Output on Linux:
[AV-1] Directory symlink on disk: …/config → ../.ssh (→ /home/node/.ssh)
[AV-1] *** SSH KEY INJECTION CONFIRMED ***
Trigger: minimal realistic path is clone (commit N) + a later pull (commit N+1) — ordinary sync of an already-trusted repo. It also collapses to a single clone when the consuming app reads or writes into the cloned tree, or when a symlink already exists on disk.
Impact
isomorphic-git is filesystem-agnostic, so exploitability and blast radius depend on the active fs provider — specifically whether it creates a symlink and follows it on a later write:
- Node
fson Linux/macOS (servers, CI, CLIs, Electron/desktop tooling — the common server-side use): real OS symlinks → the write lands anywhere the process can reach → arbitrary file write on the host → RCE (the PoC above). Highest impact. - Node
fson Windows:fs.symlinktypically requires privilege and throwsEPERM, so the symlink-plant step usually fails — not exploitable by default. (A property of the OS/provider, not of isomorphic-git.) - In-memory / browser providers (memfs, ZenFS, lightning-fs, custom
fs): there is no host filesystem to escape to, so the worst case is writing outside the intended clone directory but within that virtual filesystem — i.e. corrupting other repos/app data the same provider holds. Bounded by the sandbox, and only if the provider implements symlink creation + following.
The CVSS above reflects the worst-case (Node fs on a POSIX host). Browser/in-memory-only deployments are lower severity (sandboxed integrity impact).
Suggested fix
Before each working-tree write, lstat every leading path component and refuse to traverse a symlink (mirrors git's has_symlink_leading_path()); optionally honor core.symlinks=false.
Related Vulnerabilities
Other vulnerabilities affecting the same packages