GHSA-5xxx-qhh7-9287
GitPython: Incomplete unsafe_git_revision_options denylist omits --contents/-S, enabling arbitrary file read via Repo.blame()
Packages
Details
As written in the advisory (Markdown source, shown as plain text).
## 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 <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
`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=<secret>','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=<secret>','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
References
External links from the advisory. They lead to third-party sites.
- https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-5xxx-qhh7-9287
- https://nvd.nist.gov/vuln/detail/CVE-2026-78678
- https://github.com/gitpython-developers/GitPython
- https://github.com/pypa/advisory-database/tree/main/vulns/gitpython/PYSEC-2026-3788.yaml
- https://www.vulncheck.com/advisories/gitpython-before-arbitrary-file-read-via-repo-blame