Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
🐍
🐍 PyPI
Not in CISA KEV
HIGH severity

GitPython: Repository content can impersonate the git directory, leading to arbitrary code executionGHSA-239g-whfq-7xj9

HIGHFix: gitpython-developers/GitPython#2218

GHSA-239g-whfq-7xj9 is a high-severity (CVSS 8.8) Code Injection vulnerability in gitpython. A fix is available for gitpython — see the affected versions and patch details below.

Also known asCVE-2026-87817PYSEC-2026-3982
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 23, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

No confirmed exploitation observed yet

  • CISA assesses this as automatable — exploitation doesn’t require manual, per-target effort, which raises the odds of mass scanning and opportunistic attacks.
  • A successful exploit gives an attacker total control of the affected component, not partial access.
  • CISA’s own triage has not observed active exploitation or public proof-of-concept code for this CVE as of its last assessment.

Exploitation and automatability from CISA’s SSVC triage for GHSA-239g-whfq-7xj9.

EPSS Exploitation Probability

via FIRST.org ↗
0.4%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs34th percentile — riskier than 34% of all scored CVEsHighest risk
0.00%0.30%0.60%0.90%0.4%0.4%Oct 26Oct 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

GHSA-239g-whfq-7xj9 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
🐍gitpython

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects PyPI packages — download data is not available via public APIs for these ecosystems.

Description

Summary

Repo.__init__ decides which directory is the git directory by testing candidate paths in an order that considers the real .git last. Two earlier tests can be satisfied by ordinary tracked files. Git reserves only the literal name .git, so HEAD, objects/, refs/, config, gitdir, commondir and hooks/ at a repository root are all legal tracked content.

Consequently, after a victim opens or clones an attacker's repository, GitPython resolves git_dir to the working-tree root while real git correctly resolves <root>/.git. Everything GitPython then treats as "inside the git directory" is attacker-authored content — including hooks/, which it executes.

CVE-2026-87817

Affected code (3.1.59)

The discovery loop in git/repo/base.py tests, in order:

  1. git/repo/base.py:299 — isfile(curpath/gitdir) and isfile(curpath/commondir) and isfile(curpath/HEAD)
  2. git/repo/base.py:320 — is_git_dir(curpath)
  3. git/repo/base.py:341 — dotgit = osp.join(curpath, ".git") ← the real git dir, considered last

is_git_dir (git/repo/fun.py:60) requires only that objects/ and refs/ are directories and that HEAD is a file; HEAD's contents are never parsed. The hook path is resolved from index.repo.git_dir (git/index/fun.py:73), i.e. the mis-resolved directory.

Proof of concept

Requires only pip install GitPython==3.1.59. Full script attached as poc1_rce.py; it runs entirely in a temp directory and the payload only writes a marker file.

Attacker repository — four ordinary tracked files at the root:

pathmodecontent
gitdir100644.git\n
commondir100644.git\n
HEAD100644ref: refs/heads/master\n
hooks/pre-commit100755#!/bin/sh + payload

Victim — two ordinary calls:

repo = git.Repo.clone_from(url, dst)     # or git.Repo(dst)
repo.index.commit("automated commit")    # code execution happens here

Observed on the PyPI release 3.1.59 (Linux and Windows):

real git says the git dir is : /tmp/.../victim/.git
GitPython says it is         : /tmp/.../victim      <- shadowed
attacker's hook executed     : True
git fsck                     : (clean)

The pre-commit hook runs at git/index/base.py:1201, before write_tree(), so it fires even though the commit later fails.

Impact

A service that opens or clones an untrusted repository with GitPython — a CI runner building a fork pull request, a code-scanning/SBOM service, a mirror, a dependency bot, an AI code-review/agent tool — can be made to:

  1. Execute arbitrary commands via the tracked <root>/hooks/pre-commit when the victim calls index.commit().
  2. Read files outside the repository: the tracked <root>/config becomes the repository config and is parsed with merge_includes=True (git/repo/base.py:765), so [include] path = ~/.aws/credentials discloses the file. (Repo._config_reader still defaults merge_includes=True, so the hardening added in 3.1.59 for .gitmodules/GHSA-7833 does not cover this path.)
  3. Write a config file to an attacker-chosen directory via an absolute tracked commondir.

Delivery is silent: git clone exits 0, git fsck (including --strict, and with transfer.fsckObjects/fetch.fsckObjects=true) reports nothing, and the clone passes every read-only probe (head.commit, branches, is_dirty(), untracked_files, iter_commits) because a tracked commondir of .git pins common_dir to the real .git.

Threat model / preconditions

  • Attacker controls the content of a repository the victim opens or clones with GitPython (public repo, fork PR, mirrored dependency).
  • For code execution, the victim performs a commit via GitPython's native index.commit(). For the file-read impact, opening the repo and reading config is enough.
  • GIT_WORK_TREE is not a mitigation — git_dir is assigned and the discovery loop breaks before the environment is consulted.
  • Real git is unaffected; only GitPython mis-resolves the directory.

Remediation

  1. Test curpath/.git before the gitdir/commondir/HEAD triple and before is_git_dir(curpath).
  2. Resolve hook_path / _commit_hook_path / _get_validated_reflog_path through repo.common_dir.
  3. Containment-check commondir/gitdir contents before joining (reuse SymbolicReference._get_validated_path).
  4. Validate HEAD in is_git_dir (require ref: refs/... or a 40-hex sha).
  5. Pass merge_includes=False in Repo._config_reader (git/repo/base.py:765).

Note for the maintainers

Commit 406b98e1 (2026-05-31, "respect core.hooksPath for commit hooks") resolved the hook path via git rev-parse --git-path, which is immune because git rediscovers the true .git. Commit 9bc287a2 (2026-07-20) reverted it to avoid an unconditional rev-parse dependency — reintroducing the exec step. Measured at each commit with the healthy-looking layout: 406b98e1 → hook did not fire; 9bc287a2 … 3.1.59 → hook fired.

Scope note

Only the repository-root case is reported: where a real .git exists, git prefers it, and GitPython does not. A fake git dir in a subdirectory fools real git too, so that variant is out of scope as a general ecosystem hazard rather than a GitPython defect.

###POC Files : poc1_rce.py poc2_file_read.py

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
🐍PyPIgitpythonall versions3.1.60pip install --upgrade 'gitpython==3.1.60'

Affected Products

1 product · 1 configurations
Application
gitpythongitpython_project
< 3.1.60
range

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for gitpython, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update gitpython to 3.1.60 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-239g-whfq-7xj9 is resolved across your whole dependency graph.

  3. Workarounds

    Stop passing untrusted input into the interpreter or shell: call the affected binary with an argument array rather than a composed command string, reject anything outside a strict allowlist of expected values, and run the component under an account that cannot reach beyond the work it legitimately does.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatImportant

This vulnerability is rated as Important because it allows remote code execution in Red Hat products utilizing GitPython. Exploitation requires user interaction, specifically a victim calling index.commit() on a repository where an attacker has previously placed a malicious pre-commit hook by impersonating the git…

ProductFixed inAdvisory
Red Hat Ansible Automation Platform 2.5 for RHEL 8automation-controller-0:4.6.33-1.el8apRHSA-2026:71114
Red Hat Ansible Automation Platform 2.6 for RHEL 9automation-controller-0:4.7.17-1.el9apRHSA-2026:71113
Red Hat Satellite 6.16 for RHEL 8python-gitpython-0:3.1.62-1.el8pcRHSA-2026:74506
Red Hat Satellite 6.17 for RHEL 9python-gitpython-0:3.1.62-1.el9pcRHSA-2026:74505
Red Hat Satellite 6.18 for RHEL 9python3.12-gitpython-0:3.1.62-1.el9pcRHSA-2026:74504
Red Hat Satellite 6.19 for RHEL 9python3.12-gitpython-0:3.1.62-1.el9pcRHSA-2026:74503
Red Hat Ansible Automation Platform 2.5ansible-automation-platform-25/controller-rhel8:1789607021RHSA-2026:71210
Red Hat Ansible Automation Platform 2.6ansible-automation-platform-26/controller-rhel9:1789673739RHSA-2026:71179

Frequently Asked Questions

### Summary `Repo.__init__` decides which directory is the git directory by testing candidate paths in an order that considers the real `.git` **last**. Two earlier tests can be satisfied by ordinary tracked files. Git reserves only the literal name `.git`, so `HEAD`, `objects/`, `refs/`, `config`, `gitdir`, `commondir` and `hooks/` at a repository root are all legal tracked content. Consequently, after a victim opens or clones an attacker's repository, GitPython resolves `git_dir` to the **working-tree root** while real git correctly resolves `<root>/.git`. Everything GitPython then treats
O3 Security · Impact-Aware SCA

Is GHSA-239g-whfq-7xj9 in your dependencies?

Find it across PyPI, including transitive dependencies.

GitPython: Repository content can impersonate the git…