GitPython unsafe-option guard bypass via split_single_char_options=False short-option smuggling
A logic gap in GitPython's command-injection guard lets an attacker smuggle the dangerous --upload-pack option past the denylist by using a single-character kwarg combined with…

The problem
GitPython's check_unsafe_options guard inspects a candidate list built by _option_candidates before passing kwargs to git. That candidate list only extracts value-derived tokens for single-char keys when split_single_char_options=True.
With split_single_char_options=False, a call like Repo.clone_from(src, dst, n='utouch /tmp/ACE;git-upload-pack', split_single_char_options=False) causes the guard to see only ['-n'], which is not on the denylist. But transform_kwarg still emits the joined token -nutouch /tmp/ACE;git-upload-pack, which git clusters as -n -u<cmd>, parsing -u<cmd> as --upload-pack=<cmd> and executing it.
Every guarded method (clone_from, fetch, pull, push, ls_remote, iter_commits, blame, archive) is affected. This is a bypass of the prior fix for GHSA-r9mr-m37c-5fr3.
Proof of concept
A working proof-of-concept for this issue in GitPython, with the exact payload below.
from git import Repo
# Precondition: app forwards user-controlled kwargs to clone_from.
# Attacker-supplied kwargs dict:
# { "split_single_char_options": False, "n": "utouch /tmp/ACE;git-upload-pack" }
Repo.clone_from(
"https://example.com/repo.git",
"/tmp/dst",
n="utouch /tmp/ACE;git-upload-pack",
split_single_char_options=False, # <-- bypass key
)
# Result: /tmp/ACE is created (ACE confirmed).
# git receives argv: ['git','clone','-v','-nutouch /tmp/ACE;git-upload-pack','--','<src>','<dst>']
# git clusters: -n (no-checkout) + -u<cmd> => --upload-pack=<cmd> => executes 'touch /tmp/ACE;git-upload-pack'The root cause is in _option_candidates (cmd.py ~line 1048). The prior fix (commit e8d0fbf7 for GHSA-r9mr-m37c-5fr3) added value-token extraction only inside if len(key)==1 and split_single_char_options:. When split_single_char_options=False that branch is skipped, so the joined -n<value> form is never presented to the denylist check.
transform_kwarg (cmd.py ~line 1631) unconditionally emits the joined token regardless of the flag. Git's short-option clustering then parses -u<value> as --upload-pack=<value>, reaching the command-execution sink. The fix in commit 96a888f4 (PR #2204, released in 3.1.58) makes _option_candidates emit value-derived candidates for single-char keys unconditionally, covering both the split and joined forms so the guard always sees the full token.
The fix
Upgrade GitPython to 3.1.58 or later (patch commit 96a888f4d782cb2f80452148e48e60ce4af6d541, PR #2204). If upgrading is not immediately possible, do not forward user-controlled kwargs dicts directly to any guarded GitPython method. Sanitize or allowlist kwargs before passing them, and never permit callers to set split_single_char_options.
Reported by zx (Jace).
Related research
- high · 8.8GitPython: git-config Option-Name Injection via = and # Characters Enables RCE
- high · 8.1GitPython Argument Injection via Diffable.diff Enables Arbitrary File Overwrite
- high · 8.8GitPython: OS Command Injection via Single-Character Kwarg Value Token Smuggling
- high · 8.4GitPython Command Injection and Arbitrary File Overwrite via Unguarded Git Options