# GitPython: Repository content can impersonate the git directory, leading to arbitrary code execution

GitPython resolves a repo's git_dir to the working-tree root when tracked files mimic .git, so attacker-authored hooks/pre-commit execute on index.commit().

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: dharanivarma
- Disclosed: 2026-09-30
- CVE: CVE-2026-87817
- Write-up quality: 9/10
- Original: https://github.com/advisories/GHSA-239g-whfq-7xj9
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-239g-whfq-7xj9
- Tags: area:oss domain:oss impact:data-leak impact:rce kind:research platform:ghsa sev:high tech:python technique:regression technique:source-review vuln:rce

**Target:** GitPython Repo directory discovery / commit hook execution
**Root cause:** Repo.__init__ tests the gitdir/commondir/HEAD triple and is_git_dir(curpath) before curpath/.git, and is_git_dir never parses HEAD, so ordinary tracked files at the repo root satisfy the earlier checks and shadow the real .git. Hook paths are then resolved from the mis-resolved git_dir.
**Steps:**
1. Attacker repo root contains tracked files: `gitdir` (100644, `.git\n`), `commondir` (100644, `.git\n`), `HEAD` (100644, `ref: refs/heads/master\n`), and executable `hooks/pre-commit` (100755) with payload.
2. Victim runs `repo = git.Repo.clone_from(url, dst)` or `git.Repo(dst)`.
3. GitPython resolves git_dir to the working-tree root instead of `<root>/.git` (real git resolves correctly).
4. Victim calls `repo.index.commit("automated commit")`; pre-commit hook at `git/index/base.py:1201` executes attacker payload before write_tree().
5. For file read: tracked `<root>/config` is parsed with merge_includes=True, so `[include] path = ~/.aws/credentials` discloses the file.
**How found:** Code-reading of the discovery loop ordering in git/repo/base.py: noticed .git is tested last while earlier checks (gitdir/commondir/HEAD triple, is_git_dir) can be satisfied by legal tracked files since git reserves only the literal name .git; is_git_dir never parses HEAD contents. Traced hook path resolution to index.repo.git_dir.
**Exploitation:** Attacker publishes or forks a repo with the four tracked files; victim service clones/opens it with GitPython and performs a native index.commit(), firing the attacker's pre-commit hook. Clone exits 0 and git fsck is clean, so delivery is silent. File-read variant needs only opening the repo and reading config.
**Escalation:** Chained impacts: RCE via hooks, arbitrary file read via tracked config with merge_includes, and arbitrary config write via absolute tracked commondir.
**Insight:** When auditing libraries that reimplement git directory discovery, check the order of candidate tests and whether each check validates content (HEAD parsing) or only file existence — any check satisfiable by tracked files lets repo content shadow .git. Also check whether a prior hardening commit was reverted, reintroducing the exec path.

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

**Attacker repo files impersonating git dir**

```
| path | mode | content |
|---|---|---|
| `gitdir` | 100644 | `.git\n` |
| `commondir` | 100644 | `.git\n` |
| `HEAD` | 100644 | `ref: refs/heads/master\n` |
| `hooks/pre-commit` | **100755** | `#!/bin/sh` + payload |
```

**Victim clone and commit triggering hook**

```
repo = git.Repo.clone_from(url, dst)     # or git.Repo(dst)
repo.index.commit("automated commit")    # code execution happens here
```

