CVE-2026-78678: GitPython before 3.1.59 Arbitrary File Read via Repo.blame()
Summary Repo.blame() / Repo.blameincremental() guard forwarded revision options against unsafegitrevisionoptions, but that denylist only contains the file-WRITE options --output/-o. git blame also honors --contents <file> and -S <file>, which cause the file's lines to be echoed into the blame result — an arbitrary file READ. Neither option is in the denylist, so a caller-influenced revision value of --contents=<path> passes the guard and leaks file contents. This is a distinct sink-option and impact class (READ) from GHSA-956x-8gvw-wg5v (which addressed the blame --output WRITE), directly analogous to GHSA-539m-9xh6-q6rr (archive READ gap accepted separately from the archive write/exec advisory).
Root Cause unsafegitrevisionoptions = ["--output","-o"] (git/repo/base.py:188). The rev string is passed to optioncandidates([rev], kwargs) and placed BEFORE the -- separator (base.py:841). The canonical name of --contents=... is contents, which is not on the denylist, so no UnsafeOptionError is raised. The trailing -- protects only the pathspec, not the option before the revision.
Impact Arbitrary local file read at the privileges of the host process; the file's line contents appear in the blame result returned to the caller. Pure VALUE control (the caller forwards a user-influenced revision string). Default allowunsafeoptions=False.
Proof of Concept python result = repo.blame("--contents=/etc/passwd", "a.txt") result rows carry the victim file's line text
Attack Chain 1. Entry: app calls repo.blame(rev, file) with attacker rev="--contents=/etc/passwd" (or kwarg contents="/etc/passwd", or -S). 2. Check: Git.checkunsafeoptions(optioncandidates([rev,...], kwargs), unsafegitrevisionoptions) @ base.py:841. Guard: denylist = ["--output","-o"] only. Bypass proof: canonical name contents ∉ denylist → no error. 3. Sink: self.git.blame(rev, "--", file, p=True, ...). argv (observed): ['git','blame','-p','--contents=<secret>','HEAD','--','a.txt']. 4. Impact: blame result rows carry the victim file's line text.
Bypass Evidence Independently reproduced (independent test harness, default allowunsafeoptions=False): blame('--contents=<secret>','a.txt') → guard PASSED; result rows = ['GATESECRETLINEA','GATESECRETLINEB']. Control: blame('--output=…') still BLOCKED (guard active on this path). -S kwarg argv also reaches git unguarded.
Affected Versions GitPython <= 3.1.58 (denylist present verbatim on the latest release tag).
Suggested Fix Prefer an allowlist of blame options; at minimum add --contents/-S (and any other path-taking blame options) to unsafegitrevisionoptions, and make the membership rule "the option takes a filesystem path" rather than "the option writes output".
--- Reported by zx (Jace) — GitHub: @manus-use
Other sources
GitPython versions before 3.1.59 contain an incomplete denylist in the unsafegitrevisionoptions guard that omits --contents and -S options, allowing attackers to read arbitrary files by passing these options to Repo.blame(). Attackers can supply revision values like --contents=/etc/passwd to leak file contents through the blame result returned to the caller.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/GitPythonto a version that resolves this vulnerability.Fixed in 3.1.59 - Upgrade
Upgrade
GitPythonto a version that resolves this vulnerability.Fixed in 3.1.59 - Configuration
Modify GitPython's `unsafe_git_revision_options` denylist used by `Git.check_unsafe_options(_option_candidates(...))` (base.py:841) so that `--contents` (canonical `contents`) and `-S` are treated as unsafe (path-taking) options; the current denylist only includes `--output` and `-o`.
GitPython Repo.blame() / blame_incremental() unsafe_git_revision_options guard unsafe_git_revision_options = Include --contents and -S (and any other path-taking blame options); update membership rule to match options that take a filesystem path rather than options that write output - Compensating control
If the application passes attacker-influenced revision strings into `repo.blame(rev, file)` (e.g., `rev="--contents=/etc/passwd"` or `contents="/etc/passwd"`), ensure only a safe, fixed revision format is accepted (no leading `--`/option-style revision strings).
Event History
Frequently Asked Questions
What application behavior creates exposure to this issue?
An application is exposed when it passes attacker-controlled revision values to GitPython's Repo.blame() method. The vulnerable guard can be bypassed with revision options including --contents or -S.
What level of access does an attacker need?
The CVSS vector indicates low privileges are required. The attacker must be able to provide a value that is used as the revision argument to Repo.blame().
What is the practical impact of successful exploitation?
An attacker can use a value such as --contents=/etc/passwd to cause file contents to be returned through the blame result. The stated impact is disclosure of arbitrary readable files, with no integrity or availability impact indicated.