# GitPython validates a submodule's ".git" path only after cloning into it (RCE)

GitPython's create_submodule clones into .git/hooks before rejecting the path, planting an executable post-checkout hook that runs on later git operations.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: H1tr30xNull
- Disclosed: 2026-10-22
- CVE: CVE-2026-114145
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-83vg-56qc-22m7
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-83vg-56qc-22m7
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:command-injection vuln:path-traversal vuln:rce

**Target:** GitPython library create_submodule / .git/hooks
**Root cause:** Submodule.add() uses the permissive _to_relative_path (which only blocks .., absolute and out-of-tree paths) before cloning, while the strict _validate_repo_path that rejects .git aliases only runs at index.add time, after _clone_repo has already written to disk.
**Steps:**
1. Create evilsub/ git repo whose root contains an executable file named `post-checkout`.
2. Create victim/ repo with at least one commit and a second branch, with no sample hooks in .git/hooks (empty init.templateDir/GIT_TEMPLATE_DIR).
3. Run `git.Repo("victim").create_submodule(name="x", path=".git/hooks", url=osp.abspath("evilsub"))`, trapping ValueError.
4. Observe victim/.git/hooks/post-checkout exists with mode 0755 on all affected versions.
5. Run `git -C victim checkout <other-branch>` to trigger the hook and execute attacker code.
**How found:** Reporter noticed the 'validate before mutation' invariant from commit 43a43cd8 was broken: the strict .git-alias check lives at the index boundary (IndexFile._preprocess_add_items line 732) while the clone happens earlier in Submodule.add (line 673), so validation ordering lets the clone land first.
**Exploitation:** An app that derives the submodule path from untrusted input (user manifest or API request) is fed path=".git/hooks"; the clone writes the attacker's post-checkout payload into the live hooks directory, and any subsequent git checkout executes it. On 3.2.0+ the ValueError is raised but the hook remains; on 3.1.x no error is raised.
**Insight:** When a library adds a late validation layer for a path-traversal class, check whether any earlier mutating operation (clone, write, mkdir) still runs before that check; a raised exception after disk mutation gives false assurance. Target directories git auto-executes from, like .git/hooks, where a filename alone is the payload.

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

**create_submodule PoC planting hook in .git/hooks**

```
import git, os.path as osp
try:
    git.Repo("victim").create_submodule(name="x", path=".git/hooks", url=osp.abspath("evilsub"))
except ValueError:
    pass   # 3.2.0+: ".git" is rejected, but only AFTER the clone, so the plant already happened.
           # 3.1.x: no exception is raised at all; the call succeeds and still plants the hook.

#### victim/.git/hooks/post-checkout now exists, mode 0755, attacker-controlled, on every
#### affected version. Any later checkout runs it:
####  git -C victim checkout <other-branch>   ->   the hook runs   ->   code execution
```

