All vulnerabilities
GHSA-w8jc-g24h-crhw

GitPython: Git.ls_remote accepts ext:: URLs without check_unsafe_protocols (RCE under permissive git transport policy); merge_base / IndexFile.move lack check_unsafe_options - residual gaps of the 24439/67323 hardening rollout

GitPython
CVSS
7.5High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed inNone listedNo source names a fixed version yet
Published30 Sep 2026Updated 30 Sep 2026GitHub

What fixes it

No source in this snapshot names a fixed version.

Description

Summary

GitPython has spent 2022-2026 rolling out two reusable guards method-by-method: Git.check_unsafe_protocols(url) (raises UnsafeProtocolError for <transport>:: remote-helper URLs - CVE-2022-24439) and Git.check_unsafe_options(...) (raises UnsafeOptionError for denylisted option spellings - CVE-2023-40267, CVE-2026-42215, CVE-2026-67323). Three wrapper methods that build a git argv from caller-controlled values still have neither:

  1. **Git.ls_remote** (git/cmd.py:1128-1143) - checks options (:1139-1141) but forwards the URL positional unchecked; no allow_unsafe_protocols parameter exists. The same URL through clone_from raises UnsafeProtocolError.
  2. **Repo.merge_base** (git/repo/base.py:895-936) - forwards **kwargs/*rev to self.git.merge_base with no guard; guarded sibling iter_commits sits 12 lines up. Its exception handler additionally swallows every non-128 GitCommandError into [], masking the injection.
  3. **IndexFile.move** (git/index/base.py:1092-1168) - forwards **kwargs to git mv twice (:1147, :1167); guarded sibling remove() is 23 lines above.

Verified unfixed on main @ a54d159 (2026-09-21), post-dating latest release 3.1.62 (2026-09-07) - all three sites re-verified today.

Impact

**Site 1 (ls_remote)** - RCE conditional on the victim git transport policy. The common 'probe first, then clone' pattern (Git(...).ls_remote(attacker_url)) with an ext::sh -c ... URL executes arbitrary commands when the victim git permits the ext transport: dynamically verified with protocol.ext.allow=always (marker executed). git >= 2.38.1 stock config refuses the transport (fatal: transport 'ext' not allowed, rc 128) - GitPython adds no defense-in-depth and surfaces a bare GitCommandError. Environments on git < 2.38.1 are exposed with stock config - the exact population CVE-2022-24439 was scored against.

Sites 2/3 - latent argument-injection surface: caller kwargs flow 1:1 into the git argv before operands and -- (cmd.py:1850 opt_args + ext_args), and execute_kwargs (incl. shell=True/env - the CVE-2023-40267 Windows mechanism) passes through unguarded. Today git merge-base/git mv expose no exec option, so we claim the missing-guard class (vendor-settled by CVE-2026-67323, which bundled archive/ls_remote/iter_commits/blame in one advisory), not direct RCE.

Dynamic verification (today; GitPython main @ a54d159, git 2.55.0, Linux)

| # | Call | Result | |---|---|---| | T1 | repo.git.ls_remote('ext::/tmp/gp_touch.sh'), stock config | git refuses (rc 128); **no UnsafeProtocolError from GitPython** | | T1b | same + victim config protocol.ext.allow=always | marker created - arbitrary command executed | | T2 | repo.git.ls_remote('.', upload_pack=...) | blocked by cmd.py:1141 (options-side fixed - protocol-side is the residual gap) | | T3 | repo.merge_base('main','side', upload_pack=...) | not blocked; argv git merge-base --upload-pack=... main side; rc 129 silently converted to [] | | T4 | repo.index.move(['f1','f2'], output=...) | not blocked; option reaches git mv (rc 129) |

Coverage matrix (main @ a54d159)

check_unsafe_protocols: 7 call sites (_clone, archive, set_url, create, fetch, pull, push) - none on ls_remote. check_unsafe_options: 24 call sites incl. ls_remote, iter_commits, IndexFile.remove - absent on merge_base and move.

Precedents (same repo, same guard family)

  • CVE-2022-24439 / GHSA-hcpj-qp55-gfph (critical): check_unsafe_protocols on the clone path - Site 1 is the same ext:: primitive on a sibling API.
  • CVE-2023-40267 / GHSA-pr76-5cm5-w9cj (critical, 'incomplete fix for CVE-2022-24439'): check_unsafe_options - unguarded kwargs to argv treated as RCE (shell=True on Windows).
  • CVE-2026-42215 / GHSA-rpm5-65cw-6hj4 (high, fixed 3.1.47): underscored-spelling bypass - rollout still active in 2026.
  • CVE-2026-67323 / GHSA-956x-8gvw-wg5v (high, 8.4, fixed 3.1.51): added check_unsafe_options to archive/ls_remote/iter_commits/blame; its PoC is literally repo.git.ls_remote('.', upload_pack=...). Establishes that a wrapper missing the check is itself a vulnerability. This report is the residual gap of that rollout: ls_remote got options but not protocols; merge_base/move got neither.

Recommended fix (existing helpers, 1-3 lines per site)

  1. Git.ls_remote: if not allow_unsafe_protocols: self.check_unsafe_protocols(args[0]) + new allow_unsafe_protocols: bool = False (the _clone pattern, base.py:1536-1538).
  2. Repo.merge_base: check_unsafe_options(Git._option_candidates(list(rev), kwargs), unsafe_options=self.unsafe_git_revision_options) (the iter_commits pattern); stop masking non-128 statuses.
  3. IndexFile.move: the remove() pattern with unsafe_git_pathspec_from_file_options.

One advisory, three affected methods, severity anchored on Site 1 - mirroring CVE-2026-67323's bundling.

Text from GitHub Advisory Database

Affected versions 1

  • GitPython, pip<= 3.1.62GitHub