GHSA-956x-8gvw-wg5v GitPython: command injection via unguarded Git options in `Repo.archive()`, `git.ls_remote()`, and arbitrary file overwrite via `Repo.iter_commits()` / `Repo.blame()`
SummaryGitPython spawns the real `git` binary with an argument vector built from caller-supplied values. To prevent argument injection, GitPython maintains denylists of "unsafe" Git options (`--upload-pack`, `--receive-pack`, `--exec`, `-c`, `--config`, …) that can be abused to run arbitrary commands, and enforces them with `Git.check_unsafe_options()`.That enforcement is only wired into the **network** commands — `clone_from`, `Remote.fetch`, `Remote.pull`, `Remote.push`. Several other public APIs that also forward caller-controlled values into the `git` argv have **no guard at all**:**`Repo.archive(ostream, treeish=None, prefix=None, **kwargs)`** forwards `**kwargs` verbatim into `git archive`. An attacker-influenced options mapping such as `{"remote": ".", "exec": "<cmd>"}` becomes `git archive --remote=. --exec=<cmd> -- <treeish>`, and `git archive --remote=<local repo>` invokes `git-upload-archive` whose path is overridden by `--exec` → **arbitrary command execution under default Git configuration** (no `protocol.ext.allow` needed).**`repo.git.ls_remote(<url>, upload_pack="<cmd>")`** (and the dynamic-command builder generally) turns the `upload_pack` kwarg into `--upload-pack=<cmd>` with no guard → **arbitrary command execution**.**`Repo.iter_commits(rev)`** and **`Repo.blame(rev, file)`** place the caller's `rev` value into the argv *before* the `--` end-of-options separator and apply no leading-dash check. A benign-looking ref value such as `--output=/path/to/file` is parsed by `git rev-list` / `git blame` as the `--output` option, which **opens and truncates an arbitrary file** before Git even validates the revision → arbitrary file clobber (integrity/availability; can destroy keys, configs, lockfiles, or be aimed at files the host later sources).The first two are direct code execution; the third is an arbitrary file-overwrite primitive. All share one root cause: the `check_unsafe_options` / end-of-options discipline that GitPython applies to clone/fetch/pull/push was never extended to these sinks.DetailsGitPython explicitly recognises these options as command-execution vectors. `git/remote.py:535`:and enforces them via `Git.check_unsafe_options()` (`git/cmd.py:963`):But `check_unsafe_options` is invoked from **only five sites**, all network commands:The following sinks call `git` with caller-controlled options/positionals and are **not** guarded:1. `Repo.archive` — command execution (`git/repo/base.py:1623`)`treeish` and `path` are correctly placed after `--`, but `**kwargs` are converted by `Git.transform_kwarg` (`git/cmd.py:1487`) into `--<name>=<value>` flags and inserted **before** the `--` by `_call_process`, with no `check_unsafe_options`. `Repo.archive` already documents user-facing kwargs (`format`, `prefix`, `path`), so forwarding a caller options mapping is an expected usage. Final argv:`git archive --remote=<repo>` runs the upload-archive helper; `--exec=<cmd>` overrides the helper path, executing `<cmd>` on the host. This works with **default Git config** — it does not rely on the `ext::` transport (which is blocked by default).2. `repo.git.ls_remote(..., upload_pack=...)` — command execution (dynamic builder, `git/cmd.py:1487`)`transform_kwarg` dashifies `upload_pack` → `--upload-pack=<value>`. `git ls-remote <local-repo> --upload-pack=<cmd>` executes `<cmd>`. The dynamic builder makes **both** the flag name and value caller-controlled (`repo.git.<anything>(**user_dict)`), and `ls_remote` has no `check_unsafe_options`.This is exactly the underscore-kwarg-vs-hyphen-kwarg gap that CVE-2026-42215 fixed for `fetch`/`pull`/`push`/`clone_from` — but `ls_remote` and the rest of the dynamic surface were left unpatched.3. `Repo.iter_commits` / `Repo.blame` — arbitrary file overwrite (`git/objects/commit.py:348`, `git/repo/base.py:1199`)`rev` is placed **before** `--`, with no leading-dash check anywhere in the path. A caller passing `rev="--output=/path"` (a value that looks like an ordinary ref/branch/tag string an app forwards from user input) produces:`git rev-list`/`log`/`blame` honour `--output=<file>`, which `open()`s and truncates the file *before* validating the revision — so the file is destroyed even though Git then errors out on the bad revision.PoCAll three PoCs are self-contained, run against the released **GitPython 3.1.50** under **default Git configuration**, and were executed live (git 2.51.0). Each prints a host-side marker proving the effect.InstallPoC 1 — command execution via `Repo.archive`Verbatim output:`git config --get protocol.ext.allow` returns nothing (unset = default), confirming no special config is required.PoC 2 — command execution via `git.ls_remote(upload_pack=...)`Verbatim output:PoC 3 — arbitrary file overwrite via a benign-looking `rev`Verbatim output:
CVSS v3.1 score: 8.4