high · 8.8Jul 24, 2026

GitPython: OS Command Injection via Single-Character Kwarg Value Token Smuggling

Shubham Kandhare
Security Engagement Manager, SecureLayer7

GitPython's safety guard against dangerous git options can be completely bypassed by hiding a dangerous flag inside the value of a single-character keyword argument, letting attackers run arbitrary…

PackageGitPython
Ecosystempip
Affected<= 3.1.53
Fixed in3.1.54
GitPython: OS Command Injection via Single-Character Kwarg Value Token Smuggling

The problem

GitPython's check_unsafe_options guard inspects only kwarg KEYS when building its candidate option list. Passing a single-character kwarg (e.g. n=) causes _option_candidates to produce ['-n'], which is not on the denylist, so the guard passes silently.

However, transform_kwarg('n', '--upload-pack=cmd', split_single_char_options=True) emits TWO argv tokens: ['-n', '--upload-pack=cmd']. Git parses the second token normally and executes the attacker-supplied command. Every guarded method is affected: clone_from, fetch, pull, push, ls_remote, iter_commits, blame, and archive.

Proof of concept

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

python
from git import Repo

# Guard passes because _option_candidates sees only '-n' (the key), not the value.
# transform_kwarg then emits ['-n', '--upload-pack=touch /tmp/ACE;git-upload-pack'].
# Git executes the second token -> arbitrary command execution.
Repo.clone_from(
    "file:///path/to/bare.git",
    "/tmp/out",
    n="--upload-pack=touch /tmp/ACE;git-upload-pack"
)
# /tmp/ACE is created. Confirmed live on GitPython 3.1.53.

# File-write variant using iter_commits:
# repo.iter_commits('HEAD', g='--output=/etc/cron.d/evil')
# Candidate: ['-g'] -> not on denylist -> passes guard.
# Argv: ['-g', '--output=/etc/cron.d/evil'] -> victim file truncated/written.

The root cause is a scope mismatch between the guard and the argument transformer. _option_candidates([], kwargs) derives each candidate from the kwarg KEY only (e.g. 'n' -> '-n'), never from the value. The guard therefore never sees --upload-pack embedded in the value string.

When transform_kwarg runs afterwards with split_single_char_options=True, it detects that the value starts with -- and splits it into a separate argv token. That second token reaches git's argument parser unchecked. The patch (commit e8d0fbf, PR #2180, released in 3.1.54) fixes _option_candidates to also emit candidates derived from the values of single-character kwargs when the split behaviour is active, so check_unsafe_options evaluates the smuggled option before it ever reaches git.

CWE-88 (Argument Injection) and CWE-78 (OS Command Injection) both apply.

The fix

Upgrade GitPython to 3.1.54 or later (commit e8d0fbf, PR #2180). The patch makes _option_candidates extract and check candidates from single-character kwarg VALUES in addition to keys, closing the scope gap before transform_kwarg runs. If an immediate upgrade is not possible, avoid passing user-controlled kwargs to any GitPython method without an explicit allowlist of safe key-value pairs.

Reported by zx (Jace).

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

Related research