high · 8.8Aug 7, 2026

GitPython unsafe-option guard bypass via split_single_char_options=False short-option smuggling

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

PackageGitPython
Ecosystempip
Affected<= 3.1.57
Fixed in3.1.58
GitPython unsafe-option guard bypass via split_single_char_options=False short-option smuggling

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.

python
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).

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

Related research