Listen to this Post
Repo.blame() and Repo.blame_incremental() in GitPython use a denylist (unsafe_git_revision_options) intended to block dangerous options passed via revision strings. However, this denylist only contains `–output` and `-o` – the options that cause file write. The `git blame` command also supports `–contents -S <file>, which read a file and incorporate its lines into the blame output. Neither option is blocked, so an attacker who can influence the revision argument can supply `–contents=/etc/passwd` (or -S) and have the contents of that file echoed back in the blame result.
The root cause lies in `git/repo/base.py` where `_option_candidates` parses the revision string and checks against the denylist at line 841. The canonical name for `–contents=…` is contents, which is not in the list, so no `UnsafeOptionError` is raised. The trailing `–` separator only protects the pathspec that follows, not options preceding it. This is a pure value‑control issue: the caller forwards an attacker‑controlled revision string, and the guard fails to catch the read‑side options.
The impact is 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, effectively leaking sensitive data. The default `allow_unsafe_options=False` does not prevent this, because the guard only checks the write‑oriented options.
Proof of concept:
result = repo.blame("--contents=/etc/passwd", "a.txt")
result rows now contain lines from /etc/passwd
Attack chain:
- Entry: app calls `repo.blame(rev, file)` with `rev = “–contents=/etc/passwd”` (or via kwargs
contents="/etc/passwd", or-S). - Check: `Git.check_unsafe_options(_option_candidates([rev,…], kwargs), unsafe_git_revision_options)` (base.py:841) – denylist = `[“–output”,”-o”]` only.
- Bypass: `contents` ∉ denylist → no error.
- Sink: `self.git.blame(rev, “–“, file, p=True, …)` – actual argv:
['git','blame','-p','--contents=/etc/passwd','HEAD','--','a.txt']. - Impact: blame result rows carry the victim file’s line text.
Bypass evidence (independently reproduced with default `allow_unsafe_options=False`):
`blame(‘–contents=/etc/passwd’,’a.txt’)` → guard PASSES, result rows contain the secret.
Control: `blame(‘–output=…’)` still BLOCKED (guard active for writes).
The `-S` kwarg also reaches Git unguarded.
Affected versions: GitPython <= 3.1.58 (the denylist is identical in the latest release tag).
Suggested fix: prefer an allowlist of blame options; at minimum add `–contents` and `-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.
DailyCVE Form:
Platform: GitPython
Version: <=3.1.58
Vulnerability: Arbitrary File Read
Severity: High
date: 2026-09-08
Prediction: Next patch release
What Undercode Say:
Demonstrating the bypass with a simple Python snippet
python -c "import git; r=git.Repo('.'); print(r.blame('--contents=/etc/passwd', 'README.md'))"
Verify that --output is still blocked (guard active)
python -c "import git; r=git.Repo('.'); r.blame('--output=/tmp/out', 'README.md')"
Raises UnsafeOptionError
Alternative using -S
r.blame('-S/etc/passwd', 'a.txt')
Using kwargs form (also bypasses) r.blame(rev='HEAD', file='a.txt', contents='/etc/passwd')
Exploit: (Educational Purposes!)
import git
repo = git.Repo('/path/to/repo')
Read /etc/passwd by forcing --contents in the revision string
leaked = repo.blame('--contents=/etc/passwd', 'some_file.txt')
for commit, lines in leaked:
for line in lines:
print(line) prints lines from /etc/passwd
This works because the denylist does not catch --contents; the attacker only needs to control the `rev` parameter or pass `contents` as a keyword argument.
Protection: from this CVE
- Upgrade to a fixed version once released (GitPython > 3.1.58).
- If upgrading is not possible, sanitize any user‑supplied revision strings by rejecting arguments that contain
--contents,-S, or any other option that takes a file path. - Use an allowlist for allowed revision options instead of a denylist.
- Avoid passing untrusted input directly to `repo.blame()` or
repo.blame_incremental().
Impact:
- Arbitrary reading of any file accessible to the process (e.g.,
/etc/passwd, configuration files, source code, secrets). - Confidentiality breach: sensitive data can be exfiltrated through the blame output.
- The flaw is a bypass of the existing `allow_unsafe_options` guard, making it a direct security regression from previous fixes that only addressed write operations.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

