Patch Forecast
Yellow warning
Plan into this sprint.

GHSA-23mf-xhv8-69c2

GitPython: arbitrary file read via attacker-controlled branch name in `Head.checkout()` (incomplete fix of CVE-2026-78679)

Risk score
48/100
CVSS
7.1 v4.0
Published
27 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
> A note on the referenced files. This report was submitted through GitHub's private > vulnerability reporting API, which carries no attachments. Every transcript this report leans > on is reproduced inline above, verbatim. The file names it mentions (poc.sh, poc.log, > per-case transcripts, evidence.md, negative-cont…
Affected
GitPython (pip) <= 3.1.62
Fix
Upgrade GitPython to 3.2.0 or later.
Exploitation
No known exploitation

Story map

Story map: gitpythonTHE FLAWSTHE FIXGHSA-23mf-xhv8-69c2Arbitrary file read viaattacker-controlled branchname in `Head.checkout()`…CVSS 7.1CVE-2026-87817GitPython: Repository contentcan impersonate the gitdirectory, leading to arbitrary…CVSS 8.8 · EPSS 0%CVE-2026-114145GitPython validates asubmodule's ".git" path onlyafter cloning into it (RCE)CVSS 7.5 · EPSS 0%CVE-2026-87819Denial of Service viacatastrophic backtracking(ReDoS) in…CVSS 7.5 · EPSS 1%Upgrade gitpython3.1.58 · 3.1.59 · 3.2.1or later on your release lineWho is exposedgitpython (pip) <= 3.1.59AMBER · PATCH THIS WEEKgitpythonRepo.__init__ decides whichdirectory is the git directory bytesting candidate paths in anorder that considers the real.git last. Two earlier tests canbe satisfied by ordinary trackedfiles. Git reserves only the…Where it standsPublic proof of conceptCONNECTS TOSame weaknessCWE-73 (CWE-73)62 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

gitpython: Amber, patch this week. Repo.__init__ decides which directory is the git directory by testing candidate paths in an order that considers the real .git last. Two earlier tests can be satisfied by ordinary tracked files. Git reserves only the literal name .git, so HEAD, objects/, refs/, config, gitdir, commondir and hooks/ at a repository root…

The flaws

  • GHSA-23mf-xhv8-69c2: Arbitrary file read via attacker-controlled branch name in `Head.checkout()` (incomplete fix of CVE-2026-78679) (CVSS 7.1)
  • CVE-2026-87817: GitPython: Repository content can impersonate the git directory, leading to arbitrary code execution (CVSS 8.8 · EPSS 0%)
  • CVE-2026-114145: GitPython validates a submodule's ".git" path only after cloning into it (RCE) (CVSS 7.5 · EPSS 0%)
  • CVE-2026-87819: Denial of Service via catastrophic backtracking (ReDoS) in Actor.name_email_regex — commit author/committer field parsing (CVSS 7.5 · EPSS 1%)

The fix

Upgrade gitpython: 3.1.58 · 3.1.59 · 3.2.1 or later on your release line

Who is exposed: gitpython (pip) <= 3.1.59

Where it stands

  • Public proof of concept

Connects to

Why it is Yellow

  • CVSS 7.1 high
  • On your watchlist: PyPI

Affected packages

PackageEcosystemVulnerableFixed in
GitPythonpip<= 3.1.623.2.0

References

Original description

> **A note on the referenced files.** This report was submitted through GitHub's private > vulnerability reporting API, which carries no attachments. Every transcript this report leans > on is reproduced **inline above**, verbatim. The file names it mentions (`poc.sh`, `poc.log`, > per-case transcripts, `evidence.md`, `negative-controls.md`, blob-sweep tables) are the local > evidence bundle those inline excerpts were extracted from, with a checksum manifest. I am happy > to send the bundle by any channel you prefer, or to open a private fork — just say which. The > PoC scripts are self-contained and reproduce the result from a clean environment. --- **Affected:** `<= 3.1.62` — all published releases; also `main` @ `4905b4af` **Verified at runtime:** GitPython `3.1.62` with git `2.55.0` **Date:** 2026-09-10 **Reporter:** MAXI8594 --- ## Threat model | | | |---|---| | **Attacker** | anyone who can get the application to clone a URL they control. No account, no credential, no privilege in the consuming application. | | **Pre-need** | a repository they host, with `HEAD` pointing at a branch whose name is the payload. A `git push` to a plain git server is enough — no filesystem access to a server is required (`raw/06`). | | **Gets** | the contents of any file the application's process can read, returned in band through the exception. `/etc/passwd` recovered 34 of 34 lines. | | **Real-world likelihood** | **High.** The vulnerable consumer is six lines of ordinary code with no unusual configuration: `Repo.clone_from(user_url)` then `repo.active_branch.checkout()`. `allow_unsafe_options=False` is the default. Cloning user-supplied URLs is the library's central use case — CI systems, repository importers, scanners and bots. | **On severity relative to the sibling advisories.** GitHub, as CNA, scored the three prior advisories in this class at 6.5 / `PR:L` (CVE-2026-76217, CVE-2026-78678, CVE-2026-78679). This report asks for 7.5 / `PR:N`, and the difference is one measured fact: in those three, the application had to pass an attacker-influenced value into the API. Here it passes nothing — the clone materialises the hostile branch on its own and `repo.active_branch` returns it (`raw/02`). If that reading is not accepted, the `PR:L` path via `Repo.create_head()` is also real and lands at 6.5, matching the siblings. ## Summary `Head.checkout()` guards its `**kwargs` against unsafe Git options but never inspects its own positional argument — the branch name — which reaches the `git checkout` argv with no `--` separator. A repository whose default branch is named `--pathspec-from-file=/etc/passwd` makes a consumer that merely clones it and checks out the default branch return the entire contents of that file to the caller. This is the exact combination of two advisories the project already fixed: - **CVE-2026-76217** (`GHSA-hh9p-6wh2-4mfc`) — *"Arbitrary file read via `--pathspec-from-file` in `IndexFile.remove()` **and `Head.checkout()`**"*. Fixed in 3.1.58 by commit `f2550b65`, which added the guard to this exact method — but only for `**kwargs`. - **CVE-2026-78679** (`GHSA-3wxw-xv34-2frg`) — *"`TagReference.create` positional reference bypasses kwargs-only `--file` guard, enabling arbitrary file read (incomplete fix of 3af0c251)"*. Fixed in 3.1.59 by commit `1b0d2d9b`, which applied the positional check to `git/refs/tag.py`, `git/diff.py`, `git/index/base.py` and `git/repo/base.py` — but not to `git/refs/head.py:286`. `f2550b65` states in its own commit message that the audit found the positional variant and applied it to `HEAD.reset` — 185 lines above `Head.checkout`, in the same file, in the same commit: > "The audit also found that **reset's positional commit could carry the option before its argument > separator**. […] **Check reset's positional commit as well as keyword options**" `HEAD.reset` got the fix. `Head.checkout` did not, and `1b0d2d9b` did not come back for it. ## Affected Asset | | | |---|---| | Package | `GitPython` (PyPI) | | Affected versions | **`<= 3.1.62`** — all published releases | | Latest release tested | `3.1.62` | | Also present on | `main` @ `4905b4af` | | Vulnerable file | `git/refs/head.py` | | Guard | line 286 — `Git._option_candidates([], kwargs)` | | Sink | line 293 — `self.repo.git.checkout(self, **kwargs)` | | CWE | CWE-88 (Argument Injection), CWE-73 (External Control of File Name or Path), CWE-200 (Exposure of Sensitive Information) | | CVSS 3.1 | `AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` → **7.5 High** | The version range was established by **blob hash against every tag**, not by dates: the sink line `self.repo.git.checkout(self, **kwargs)` — with no `--` separator — is present in `git/refs/head.py` in every tag from `0.3.1-beta2` through `3.1.62`. The `3.1.62` blob is byte-identical to `main`: `7f563374ef9e018b3d5f95054498e8591e112ce2`. Full sweep in `raw/07-blob-version-sweep.txt`. The kwargs-only guard first appears in **3.1.58**, so `3.1.58`–`3.1.62` is where "incomplete fix" applies; earlier releases have the same sink with no guard at all. ## Vulnerability Details ### Root cause `git/cmd.py:1045` — `_option_candidates()` only inspects the positionals it is handed: ```python @classmethod def _option_candidates(cls, args: Sequence[Any] = (), kwargs: Optional[Mapping[str, Any]] = None) -> List[str]: """Collect possible option spellings before command-line transformation.""" options = [ option for option in cls._unpack_args([arg for arg in args if arg is not None]) if option.startswith("-") ] ``` So `_option_candidates([], kwargs)` means: *check the keyword arguments, never check the positional arguments this method puts on the command line.* That is fine when the method has no attacker-influenced positional, or when it separates them with `--`. `git/refs/head.py:283-293` — `Head.checkout()` has neither: ```python if not allow_unsafe_options: Git.check_unsafe_options( opti

Last activity 27 Oct 2026. Patch Forecast updates every hour.