CVE-2026-78675: GitPython Local File Content Disclosure via .gitmodules [include] Directive
Any tool that reads submodules from an untrusted Git repository can have the first line of any local file, think .env secrets or /etc/passwd, leaked into a Python exception message, just by calling…
![CVE-2026-78675: GitPython Local File Content Disclosure via .gitmodules [include] Directive](https://securelayer7.net/media/lab-a346a0a7.webp)
The problem
GitPython's SubmoduleConfigParser, used for every read of a repo's submodule configuration, inherits merge_includes=True from GitConfigParser. This means any [include] or [includeIf] directive inside a .gitmodules file is followed and merged automatically.
.gitmodules is fully attacker-controlled: it ships verbatim as tracked content inside any cloned repository. An attacker adds an [include] path = /etc/passwd (or any absolute path) to .gitmodules. When a victim calls repo.submodules, GitConfigParser opens the target file and, because it is not valid git-config syntax, raises configparser.MissingSectionHeaderError with that file's first line embedded verbatim in the exception message.
Submodule.iter_items() catches only IOError and BadName, not configparser.Error, so the exception propagates straight out to the caller, CI logs, error trackers, or any surface that echoes exceptions.
Proof of concept
A working proof-of-concept for CVE-2026-78675 in GitPython, with the exact payload below.
# Attacker plants this in their repo's .gitmodules before pushing/publishing:
[submodule "totally-normal-dep"]
path = vendor/dep
url = https://example.com/dep.git
[include]
path = /etc/passwd
# Victim runs any of these (no submodule update/init needed):
# import git, configparser
# repo = git.Repo.clone_from("https://attacker.example/repo", "/tmp/victim")
# try:
# list(repo.submodules) # <-- triggers the bug
# except configparser.MissingSectionHeaderError as e:
# print(e) # prints: first line of /etc/passwd verbatim
#
# Relative traversal also works (no OS-level restriction applies):
# [include]
# path = ../../../../home/user/.envThe root cause is a parity gap between two config-parser construction sites. In 2023, commit 41ecc6a4 hardened Repo.config_writer() by passing merge_includes=False to its GitConfigParser. The fix never touched Submodule._config_parser(), which constructs SubmoduleConfigParser(fp_module, read_only=read_only) with no merge_includes argument, so the class default of True is inherited unchanged.
GitConfigParser.read()'s include-path resolution performs zero containment checks. An absolute path short-circuits the join entirely via osp.isabs(); a relative path is joined with osp.dirname(file_path) and normpath'd with no check that the result stays inside the repository.
The only gate before opening is os.access(include_path, os.R_OK), a readability check, not a path restriction.
Once opened, _read() raises configparser.MissingSectionHeaderError(fpname, lineno, line), and Python's stdlib formats that exception's str() to embed the raw first line of the target file. Because Submodule.iter_items() never catches configparser.Error, the message propagates directly to the caller.
CWE-73 (External Control of File Name or Path) and CWE-200 (Exposure of Sensitive Information) both apply. The fix in commit ef7568e3 passes merge_includes=False at the SubmoduleConfigParser construction site in git/objects/submodule/base.py, mirroring the 2023 hardening.
The fix
Upgrade to GitPython 3.1.59 or later. The patch (commit ef7568e3b317ce617eacda39b8b54dcdff8c3b5c, PR #2211) passes merge_includes=False when constructing SubmoduleConfigParser in Submodule._config_parser(), preventing .gitmodules from following any [include] or [includeIf] directives.
If you cannot upgrade immediately, avoid calling repo.submodules, Submodule.iter_items(), or any Submodule.config() read on repositories cloned from untrusted sources.
Related research
- high · 7.5CVE-2026-78677CVE-2026-78677: GitPython clone_from() Path Traversal via --separate-git-dir
- high · 8.2GitPython Submodule Name Path Traversal to Arbitrary Git Repository Creation
- high · 7.5GitPython Environment-Variable Exfiltration via Repo.create_remote() URL
- critical · 9.8CVE-2026-78676CVE-2026-78676: GitPython Multi-line Config Value RCE via Unsafe Re-serialization