{"id":"GHSA-9qw7-j9xw-fv9c","summary":"isomorphic-git: Arbitrary file write outside of working tree via directory symlink during checkout (potential RCE)","details":"### Summary\n`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.\n\n### Details\nA single git tree can't hold a name as both a symlink and a directory, so the attack uses two states:\n\n```\nCommit N:    config            (mode 120000, symlink → ../.ssh)\nCommit N+1:  config/           (mode 040000, tree)\n               authorized_keys (mode 100644, attacker public key)\n```\n\nChecking out commit N writes `config` as a directory symlink. Checking out commit N+1 sees `config/authorized_keys` as a new path: isomorphic-git `mkdir`s the parent (`config`, already a symlink → `EEXIST`, ignored) then `writeFile`s 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.\n\nThe 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.\n\n#### Canonical git behavior (divergence)\ngit 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.\n\n### PoC\n`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:\n\n```\n[AV-1] Directory symlink on disk: …/config → ../.ssh  (→ /home/node/.ssh)\n[AV-1] *** SSH KEY INJECTION CONFIRMED ***\n```\n\n**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.\n\n\n### Impact\n\nisomorphic-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:\n\n- **Node `fs` on 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.\n- **Node `fs` on Windows:** `fs.symlink` typically requires privilege and throws `EPERM`, so the symlink-plant step usually fails — not exploitable by default. (A property of the OS/provider, not of isomorphic-git.)\n- **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.\n\nThe CVSS above reflects the worst-case (Node `fs` on a POSIX host). Browser/in-memory-only deployments are lower severity (sandboxed integrity impact).\n\n### Suggested fix\nBefore each working-tree write, `lstat` every leading path component and refuse to traverse a\nsymlink (mirrors git's `has_symlink_leading_path()`); optionally honor `core.symlinks=false`.","aliases":["CVE-2026-63395"],"modified":"2026-11-09T20:00:04.157950335Z","published":"2026-11-09T19:53:44Z","database_specific":{"github_reviewed":true,"github_reviewed_at":"2026-11-09T19:53:44Z","nvd_published_at":"2026-11-03T20:18:14Z","cwe_ids":["CWE-22","CWE-59"],"severity":"HIGH"},"references":[{"type":"WEB","url":"https://github.com/isomorphic-git/isomorphic-git/security/advisories/GHSA-9qw7-j9xw-fv9c"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-63395"},{"type":"WEB","url":"https://github.com/isomorphic-git/isomorphic-git/pull/2370"},{"type":"WEB","url":"https://github.com/isomorphic-git/isomorphic-git/commit/823843d1cd19182df79bc907c767f5bcecb39a0b"},{"type":"PACKAGE","url":"https://github.com/isomorphic-git/isomorphic-git"},{"type":"WEB","url":"https://github.com/isomorphic-git/isomorphic-git/releases/tag/v1.38.6"}],"affected":[{"package":{"name":"isomorphic-git","ecosystem":"npm","purl":"pkg:npm/isomorphic-git"},"ranges":[{"type":"SEMVER","events":[{"introduced":"0"},{"fixed":"1.38.6"}]}],"database_specific":{"last_known_affected_version_range":"\u003c= 1.38.5","source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-9qw7-j9xw-fv9c/GHSA-9qw7-j9xw-fv9c.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H"}]}