GitHub Security Advisory: GHSA-rpm5-65cw-6hj4
Package: GitPython (PIP)
Severity: HIGH (CVSS 8.8)
CWE: CWE-78: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')
Vulnerable versions: >= 3.1.30, < 3.1.47
Patched in: 3.1.47

Summary: GitPython has Command Injection via Git options bypass

Details:
### Summary
GitPython blocks dangerous Git options such as `--upload-pack` and `--receive-pack` by default, but the equivalent Python kwargs `upload_pack` and `receive_pack` bypass that check. If an application passes attacker-controlled kwargs into `Repo.clone_from()`, `Remote.fetch()`, `Remote.pull()`, or `Remote.push()`, this leads to arbitrary command execution even when `allow_unsafe_options` is left at its default value of `False`.

### Details
GitPython explicitly treats helper-command options as unsafe because they can be used to execute arbitrary commands:

- `git/repo/base.py:145-153` marks clone options such as `--upload-pack`, `-u`, `--config`, and `-c` as unsafe.
- `git/remote.py:535-548` marks fetch/pull/push options such as `--upload-pack`, `--receive-pack`, and `--exec` as unsafe.

The vulnerable API paths check the raw kwarg names before they're its normalized into command-line flags:

- `Repo.clone_from()` checks `list(kwargs.keys())` in `git/repo/base.py:1387-1390`
- `Remote.fetch()` checks `list(kwargs.keys())` in `git/remote.py:1070-1071`
- `Remote.pull()` checks `list(kwargs.keys())` in `git/remote.py:1124-1125`
- `Remote.push()` checks `list(kwargs.keys())` in `git/remote.py:1197-1198`

That validation is performed by `Git.check_unsafe_options()` in `git/cmd.py:948-961`. The validator correctly blocks option names such as `upload-pack`, `receive-pack`, and `exec`.

Later, GitPython converts Python kwargs into Git command-line flags in `Git.transform_kwarg()` at `git/cmd.py:1471-1484`. During that step, underscore-form kwargs are dashified:

- `upload_pack=...` becomes `--upload-pack=...`
- `receive_pack=...` becomes `--receive-pack=...`

Because the unsafe-option check runs before this normalization, underscore-form kwargs bypass the safety check even though they become the exact dangerous Git flags that the code is supposed to reject.

In practice:

- `remote.fetch(**{"upload-pack": helper})` is blocked with `UnsafeOptionError`
- `remote.fetch(upload_pack=helper)` is allowed and reaches helper execution

The same bypass works for:

```python
Repo.clone_from(origin, out, upload_pack=helper)
repo.remote("origin").fetch(upload_pack=helper)
repo.remote("origin").pull(upload_pack=helper)
repo.remote("origin").push(receive_pack=helper)
```

This does not appear to affect every unsafe option. For example, `exec=` is already rejected because the raw kwarg name `exec` matches the blocked option name before normalization.

Existing tests cover the hyphenated form, not the vulnerable underscore form. For example:

- `test/test_clone.py:129-136` checks `{"upload-pack": ...}`
- `test/test_remote.py:830-833` checks `{"upload-pack": ...}`
- `test/test_remote.py:968-975` checks `{"receive-pack": ...}`

Those tests correctly confirm the literal Git option names are blocked, but they do not exercise the normal Python kwarg spelling that bypasses the guard.

### PoC
1. Create and activate a virtual environment in the repository root:

```bas