Live › vulnerability › GHSA-w8jc-g24h-crhw
Go to dashboardGitPython's ls_remote, merge_base and IndexFile.move forward caller-supplied URLs and options straight to git without the library's safety guards. On systems where git allows the ext transport, a crafted URL passed to ls_remote can run arbitrary shell commands, and the other two methods let attacker-chosen options reach git.
Summary written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-27 15:28 UTC. All times on this page are UTC.
Applications that call GitPython's ls_remote with a URL an outside user can influence, and that run on git versions older than 2.38.1 or with the ext transport allowed, are at risk of command execution. Code that only passes fixed, trusted URLs and options, or that only uses merge_base or move with internal values, has a much smaller exposure. Teams on managed hosting with modern git defaults face mainly argument injection.
| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| GitPython | pypi | <= 3.1.62 | 3.2.0 |
pip show GitPython and check whether the installed version is 3.1.62 or oldergit --version; git 2.38.1 or newer refuses the ext transport by default, so exposure is mainly on older git or where protocol.ext.allow=always is set3.2.0Git.ls_remote, Repo.merge_base or IndexFile.moveEach row is the first time that source carried this signal. The analyst’s rows are its own findings.
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:
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.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.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.
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.
| # | 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=...) |
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.
check_unsafe_protocols on the clone path - Site 1 is the same ext:: primitive on a sibling API.check_unsafe_options - unguarded kwargs to argv treated as RCE (shell=True on Windows).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.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).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.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.
This page as .md · .json · in your agent: get_signal("GHSA-w8jc-g24h-crhw")
GitHub Advisory Database, CC-BY 4.0. Posts quoted belong to their authors.
not blocked; option reaches git mv (rc 129) |