GHSA-9QW7-J9XW-FV9C

Vulnerability from github – Published: 2026-11-09 19:53 – Updated: 2026-11-09 19:53
VLAI
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 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.
  • 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.)
  • 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.

Show details on source website

{
  "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)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
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.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.