Patch Forecast
Yellow warning
Plan into this sprint.

GHSA-w8jc-g24h-crhw

GitPython command wrappers omit unsafe protocol and option checks

Risk score
55/100
CVSS
7.5 v3.1
Published
27 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
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…
Affected
GitPython (pip) <= 3.1.62
Fix
Upgrade GitPython to 3.2.0 or later.
Exploitation
No known exploitation

Story map

Story map: gitpythonTHE FLAWSTHE FIXGHSA-w8jc-g24h-crhwGitPython command wrappersomit unsafe protocol and optionchecksCVSS 7.5CVE-2026-87817GitPython: Repository contentcan impersonate the gitdirectory, leading to arbitrary…CVSS 8.8 · EPSS 0%CVE-2026-114145GitPython validates asubmodule's ".git" path onlyafter cloning into it (RCE)CVSS 7.5 · EPSS 0%CVE-2026-87819Denial of Service viacatastrophic backtracking(ReDoS) in…CVSS 7.5 · EPSS 1%Upgrade gitpython3.2.1or later on your release lineWho is exposedgitpython (pip) <= 3.1.59AMBER · PATCH THIS WEEKgitpythonRepo.__init__ decides whichdirectory is the git directory bytesting candidate paths in anorder that considers the real.git last. Two earlier tests canbe satisfied by ordinary trackedfiles. Git reserves only the…Where it standsPublic proof of conceptCONNECTS TOSame weaknessCWE-1287 (CWE-1287)8 other warnings in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

gitpython: Amber, patch this week. Repo.__init__ decides which directory is the git directory by testing candidate paths in an order that considers the real .git last. Two earlier tests can be satisfied by ordinary tracked files. Git reserves only the literal name .git, so HEAD, objects/, refs/, config, gitdir, commondir and hooks/ at a repository root…

The flaws

  • GHSA-w8jc-g24h-crhw: GitPython command wrappers omit unsafe protocol and option checks (CVSS 7.5)
  • CVE-2026-87817: GitPython: Repository content can impersonate the git directory, leading to arbitrary code execution (CVSS 8.8 · EPSS 0%)
  • CVE-2026-114145: GitPython validates a submodule's ".git" path only after cloning into it (RCE) (CVSS 7.5 · EPSS 0%)
  • CVE-2026-87819: Denial of Service via catastrophic backtracking (ReDoS) in Actor.name_email_regex — commit author/committer field parsing (CVSS 7.5 · EPSS 1%)

The fix

Upgrade gitpython: 3.2.1 or later on your release line

Who is exposed: gitpython (pip) <= 3.1.59

Where it stands

  • Public proof of concept

Connects to

Why it is Yellow

  • CVSS 7.5 high
  • Remotely exploitable without logging in
  • On your watchlist: PyPI

Affected packages

PackageEcosystemVulnerableFixed in
GitPythonpip<= 3.1.623.2.0

References

Original 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.

Last activity 27 Oct 2026. Patch Forecast updates every hour.