GitHub Advisoryadvisory · PyPI
GHSA-5xxx-qhh7-9287 GitPython: Incomplete unsafe_git_revision_options denylist omits --contents/-S, enabling arbitrary file read via Repo.blame()
MEDIUM
Official page ↗
- Aliases
- CVE-2026-78678
- Ecosystem
- PyPI
- Packages
- GitPython
- Severity
- Moderate
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N
- Weaknesses
- CWE-200, CWE-88
- Reviewed
- 2026-09-08
Affected packages
• PyPI GitPython: < 3.1.59 → fixed in 3.1.59
Description
Summary
`Repo.blame()` / `Repo.blame_incremental()` guard forwarded revision options against `unsafe_git_revision_options`, but that denylist only contains the file-WRITE options `--output`/`-o`. `git blame` also honors `--contents ` and `-S `, 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=` 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
`unsafe_git_revision_options = ["--output","-o"]` (`git/repo/base.py:188`). The `rev` string is passed to `_option_candidates([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 `allow_unsafe_options=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.check_unsafe_options(_option_candidates([rev,...], kwargs), unsafe_git_revision_options)` @ 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=','HEAD','--','a.txt']`.
4. Impact: blame result rows carry the victim file's line text.
Bypass Evidence
Independently reproduced (independent test harness, default `allow_unsafe_options=False`): `blame('--contents=','a.txt')` → guard PASSED; result rows = `['GATE_SECRET_LINE_A','GATE_SECRET_LINE_B']`. 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 `unsafe_git_revision_options`, and make the membership rule "the option takes a filesystem path" rather than "the option writes output".
---
Reported by zx (Jace) — GitHub: @manus-use
Related
Alias · CVE (1)
- CVE-2026-78678 GitPython versions before 3.1.59 contain an incomplete denylist in the unsafe_git_revision_options guard that omits --contents and -S… vulnerability
References
- https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-5xxx-qhh7-9287 web
- https://nvd.nist.gov/vuln/detail/CVE-2026-78678 advisory
- https://github.com/gitpython-developers/GitPython package
- https://github.com/pypa/advisory-database/tree/main/vulns/gitpython/PYSEC-2026-3788.yaml web
- https://www.vulncheck.com/advisories/gitpython-before-arbitrary-file-read-via-repo-blame web