GitPython submodule update path traversal can write outside the repositoryGHSA-59cr-6r3x-644w
Fix: gitpython-developers/GitPython#2225GHSA-59cr-6r3x-644w is a Path Traversal vulnerability in gitpython. A fix is available for gitpython — see the affected versions and patch details below.
Exploitation Status
No confirmed exploitation observed yet
- 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-59cr-6r3x-644w.
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
Affected: GitPython 3.1.61 (latest release) and main — git/objects/submodule/base.py. git diff 3.1.61 origin/main -- git/objects/submodule/ is empty, so both are identical here.
The gap
The fix for GHSA-hmq2-w58f-27jc added Submodule._validated_name() and wired it into update() and five siblings, closing the .gitmodules name → .git/modules/<name> traversal. The other attacker-controlled .gitmodules field, path, is read raw:
# git/objects/submodule/base.py:172-177
def _set_cache_(self, attr):
if attr in ("path", "_url", "_branch_path"):
reader = self.config_reader()
self.path = reader.get("path") # raw .gitmodules value
and GitPython's own containment guard is applied in only two of the places that consume it:
400: def _to_relative_path(cls, parent_repo, path) # the guard (abspath + commonpath containment)
542: path = cls._to_relative_path(repo, path) # add() — guarded
1041: module_checkout_path = self._to_relative_path(self.repo, module_path) # move() — guarded
update() validates only the name and then uses the path-derived absolute location directly:
788: self._validated_name(self.name) # NAME only
801: checkout_module_abspath = self.abspath # derived from self.path — unguarded
821: os.makedirs(checkout_module_abspath, exist_ok=True)
So path = ../../../tmp/escaped in an attacker-authored .gitmodules selects the directory that gets created and, on the clone path, populated from the submodule URL. The same absolute location is what force_remove hands to shutil.rmtree.
The asymmetry is the argument: this is not a missing concept — the project wrote _to_relative_path() precisely for this, and add()/move() use it. update() does not.
Honest limits (please read before rating)
- The most common flow is not affected.
Repo.clone_from(...)→repo.submodules→sm.update(init=True)re-derivespathfrom a canonical tree lookup, and real git refuses to check out a tree containing a..component, so an evil.gitmodulesnever lands in the working tree in the first place. A reachable trigger therefore requires the victim's code to name a non-HEAD commit (a historical-commit API such assubmodule_update(previous_commit=...)). - The researcher did not build that end-to-end trigger. The researcher only verified first-hand the code above: the guard's two call sites, the name-only validation in
update(), and the unguardedabspath→os.makedirs()flow at 3.1.61 ==main.
Suggested fix
Apply the guard the project already has, wherever the path is consumed:
# in update(), before deriving abspath (and in any other consumer of self.path):
checkout_rel = self._to_relative_path(self.repo, self.path) # raises if it escapes the working tree
Better still, validate at the boundary: reject a .gitmodules entry whose path is absolute or contains a .. component when the section is first read in _set_cache_()/iter_items(), so no consumer can be added later without the check. A regression test with path = ../escaped alongside the existing name test would pin both fields.
Prior art checked
GHSA-hmq2-w58f-27jc (this is a residual of its fix, in the sibling field, not a re-report) plus the repository's 30 published advisories — none mentions the path field or _to_relative_path. Searched issues and PRs for _to_relative_path, gitmodules path and submodule traversal: no report of this.
Credit
kta1kri.
Appendix — EVIDENCE_gitpython_path_unguarded_20260901.txt (inlined; advisories accept no attachments)
=== EVIDENCE: GitPython — the .gitmodules 'path' field reaches os.makedirs()/clone unguarded ===
Mon Aug 31 18:45:22 UTC 2026
--- artifact: tag 3.1.61 (latest release); git diff 3.1.61 origin/main -- git/objects/submodule/ is empty ---
--- the containment guard GitPython owns, and its only two call sites ---
33: _to_relative_path,
400: def _to_relative_path(cls, parent_repo: "Repo", path: PathLike) -> PathLike:
407: path = _to_relative_path(parent_repo.working_tree_dir, path)
542: path = cls._to_relative_path(repo, path)
1041: module_checkout_path = self._to_relative_path(self.repo, module_path)
--- the parent fix (_validated_name) call sites: it validates the NAME ---
309: def _validated_name(cls, name: str) -> str:
321: name = cls._validated_name(name)
541: cls._validated_name(name)
788: self._validated_name(self.name)
1040: self._validated_name(self.name)
1181: self._validated_name(self.name)
1439: self._validated_name(self.name)
1440: self._validated_name(new_name)
1489: self._validated_name(self.name)
--- update(): name validated, path not; abspath -> os.makedirs ---
try:
self._validated_name(self.name)
# ENSURE REPO IS PRESENT AND UP-TO-DATE
# END early abort if init is not allowed
checkout_module_abspath = self.abspath
module_abspath = self._module_abspath(self.repo, self.path, self.name)
# ``git submodule deinit`` leaves the repository in
# ``.git/modules`` and empties the checkout. Reconnect that retained
# repository instead of trying to clone over it.
if not dry_run and osp.isdir(module_abspath):
try:
git.Repo(module_abspath)
except InvalidGitRepositoryError:
pass
else:
if osp.lexists(checkout_module_abspath) and (
osp.islink(checkout_module_abspath)
or not osp.isdir(checkout_module_abspath)
or os.listdir(checkout_module_abspath)
):
raise OSError(
"Module directory at %r does already exist and is non-empty" % checkout_module_abspath
)
os.makedirs(checkout_module_abspath, exist_ok=True)
self._write_git_file_and_module_config(checkout_module_abspath, module_abspath)
mrepo = git.Repo(checkout_module_abspath)
--- where self.path comes from (raw .gitmodules value) ---
def _set_cache_(self, attr: str) -> None:
if attr in ("path", "_url", "_branch_path"):
reader: SectionConstraint = self.config_reader()
# Default submodule values.
try:
self.path = reader.get("path")
except cp.NoSectionError as e:
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 🐍PyPI | gitpython | all versions | 3.1.62pip install --upgrade 'gitpython==3.1.62' |
Detection & 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.62 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-59cr-6r3x-644w 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.
This vulnerability is evaluated as having Moderate impact on Red Hat products because successful exploitation requires processing an untrusted repository alongside invoking submodule operations on non-HEAD revisions. Default usage and standard clone workflows are not exposed to this path traversal flaw, as underlying…
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base, or stability.Source: Red Hat security advisory for GHSA-59cr-6r3x-644w (CC BY 4.0)
Frequently Asked Questions
Is GHSA-59cr-6r3x-644w in your dependencies?
Find it across PyPI, including transitive dependencies.