GHSA-9QW7-J9XW-FV9C
Vulnerability from github – Published: 2026-11-09 19:53 – Updated: 2026-11-09 19:53Summary
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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.38.5"
},
"package": {
"ecosystem": "npm",
"name": "isomorphic-git"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.38.6"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-63395"
],
"database_specific": {
"cwe_ids": [
"CWE-22",
"CWE-59"
],
"github_reviewed": true,
"github_reviewed_at": "2026-11-09T19:53:44Z",
"nvd_published_at": "2026-11-03T20:18:14Z",
"severity": "HIGH"
},
"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\u0027s 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` \u2192 remote login / code execution.\n\n### Details\nA single git tree can\u0027t hold a name as both a symlink and a directory, so the attack uses two states:\n\n```\nCommit N: config (mode 120000, symlink \u2192 ../.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 \u2192 `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`\u2192`100644` mode change triggers an `rm`), but the **directory**-level variant is not \u2014 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\u0027s own control directory \u2014 `hooks \u2192 .git/hooks` then `hooks/pre-commit`, or `x \u2192 .git` then overwriting `.git/config` (`core.hooksPath` / alias / `core.fsmonitor`) \u2014 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 \u2014 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` \u2014 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: \u2026/config \u2192 ../.ssh (\u2192 /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) \u2014 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 \u2014 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 \u2014 the common server-side use): real OS symlinks \u2192 the write lands anywhere the process can reach \u2192 **arbitrary file write on the host \u2192 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 \u2014 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** \u2014 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\u0027s `has_symlink_leading_path()`); optionally honor `core.symlinks=false`.",
"id": "GHSA-9qw7-j9xw-fv9c",
"modified": "2026-11-09T19:53:44Z",
"published": "2026-11-09T19:53:44Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H",
"type": "CVSS_V3"
}
],
"summary": "isomorphic-git: Arbitrary file write outside of working tree via directory symlink during checkout (potential RCE)"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
How durations are calculated
Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.
Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.