# GitPython Remote.pull() refspec bypasses the --upload-pack unsafe-option guard, enabling arbitrary command execution

GitPython's Remote.pull() unsafe-option guard only screens kwargs, so an attacker-controlled positional refspec like --upload-pack=<cmd> reaches git pull and executes commands over local/SSH transports.

- Payout: Research
- Severity: high
- Program: GitPython
- Found by: manus-pi, manus-use
- Disclosed: 2026-10-27
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-f9j4-qggq-h239
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-f9j4-qggq-h239
- Tags: area:oss domain:oss impact:rce kind:research platform:ghsa sev:high tech:python technique:incomplete-fix technique:source-review vuln:command-injection

**Target:** GitPython library Remote.pull() refspec argument / git pull subprocess
**Root cause:** Remote.pull()'s check_unsafe_options builds candidates via Git._option_candidates([], kwargs), passing an empty positional list, so the positional refspec is never compared against unsafe_git_pull_options. The value is forwarded after `--`, but git pull re-forwards trailing positionals to an internal git fetch without re-inserting `--`, so the option is honored.
**Steps:**
1. Create an origin repo reachable over local-path (protocol.file.allow=always for git >= 2.38) or SSH transport.
2. In a consumer repo, register that origin and call Remote.pull() with attacker-controlled refspec.
3. Pass `origin.pull(refspec="--upload-pack=touch /tmp/pwned; git-upload-pack")` with default allow_unsafe_options=False.
4. Observe cmdline `git pull -v -- origin --upload-pack=touch /tmp/pwned; git-upload-pack` and marker file created.
**How found:** Noticed the guard calls _option_candidates with an empty positional list, then empirically compared the same value as `upload_pack=` kwarg (raises UnsafeOptionError) versus positional refspec (does not). Tested the `--` separator as mitigation and refuted it: git pull re-forwards positionals to internal fetch without `--`, unlike git fetch/git push which honor it.
**Exploitation:** Attacker supplies refspec string `--upload-pack=<command>; git-upload-pack` through an app parameter (branch/refspec). It passes _unpack_args and check_unsafe_protocols (no `<transport>::` form), escapes the keyword-only guard, and is forwarded to git pull. git pull re-forwards it to internal git fetch without `--`, so git runs <command> as the upload-pack program for local-path or SSH transports, executing with the calling process's privileges.
**Insight:** When a library guards dangerous git options, check whether the guard inspects positionals or only kwargs, and whether a `--` terminator actually survives re-forwarding: git pull re-invokes git fetch internally and drops the separator, so option-shaped positionals still become options.

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

**Python PoC calling pull with malicious refspec**

```
import git
repo = git.Repo("consumer")                 # has an origin using a local-path or ssh transport
origin = repo.remote("origin")
# Attacker-controlled refspec value; default arguments (allow_unsafe_options=False):
origin.pull(refspec="--upload-pack=touch /tmp/pwned; git-upload-pack")
```

**Resulting git pull command line and test outcomes**

```
cmdline: git pull -v -- origin --upload-pack=touch /tmp/pwned; git-upload-pack
[attack]  marker created (RCE)      : True
[isolate] upload_pack passed as KWARG blocked : True   (only the positional refspec escapes)
[sibling] same value via git fetch  : safe            (pull-specific behavior)
[control] benign refspec            : safe
```

