GitHub Advisoryadvisory · PyPI

GHSA-r9mr-m37c-5fr3 GitPython: Unsafe git option guard bypass via single-character kwarg value token smuggling enables arbitrary command execution

HIGH Published 2026-07-24 Modified 2026-07-24 Official page ↗
Ecosystem
PyPI
Packages
GitPython
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Weaknesses
CWE-78, CWE-88
Reviewed
2026-07-24

Affected packages

• PyPI GitPython: < 3.1.54 → fixed in 3.1.54

Description

Summary GitPython's `check_unsafe_options` guard (the control introduced by CVE-2026-42215 / GHSA-2f96 and hardened since) can be bypassed for every guarded method (`clone`/`clone_from`, `fetch`/`pull`/`push`, `ls_remote`, `iter_commits`, `blame`, `archive`) by smuggling an option token inside the VALUE of a single-character kwarg. In the default `allow_unsafe_options=False` configuration this yields arbitrary command execution via `--upload-pack`. Root Cause The guard builds its candidate option list from kwarg KEYS only: `_option_candidates([], {"n":"--upload-pack="})` returns `['-n']` (cmd.py:1042-1046 derives the candidate from the key, never the value). `-n` is not on the denylist, so `check_unsafe_options` passes. But `transform_kwarg('n', value, split_single_char_options=True)` (cmd.py:1600-1606) emits two argv tokens `['-n', '--upload-pack=']`. git then parses the second token as `--upload-pack` and executes the attacker-supplied command. The guard never inspects the value that becomes a separate argv token. Impact Arbitrary OS command execution as the host process (via `--upload-pack`) in the default configuration, affecting all guarded methods since they all build candidates through the name-only `_option_candidates`. Proof of Concept ```python from git import Repo Repo.clone_from(bare_repo, out_dir, n="--upload-pack=touch /tmp/ACE;git-upload-pack") /tmp/ACE created -> ACE. Direct-name form upload_pack="..." is correctly BLOCKED. ``` File-write variant on a guarded revision command: `iter_commits('HEAD', g='--output=/path')` -> candidate `['-g']` passes, argv `['-g','--output=/path']`, victim file truncated. Attack Chain 1. Entry: app forwards a user-supplied options dict -> `Repo.clone_from(url, path, n="--upload-pack=touch /tmp/ACE;git-upload-pack")`. Guard: `check_unsafe_options(options=_option_candidates([], kwargs), unsafe=unsafe_git_clone_options)` at base.py. Bypass proof: `_option_candidates([], {"n":"--upload-pack=..."})` -> `['-n']` (key-only), not on denylist -> no UnsafeOptionError (verified live). 2. Transform: `transform_kwarg('n', value, split_single_char_options=True)` -> `['-n', '--upload-pack=touch /tmp/ACE;git-upload-pack']`. Guard: none (guard already passed on name-only candidate). Bypass proof: verified transform emits two tokens. 3. Sink: `git clone -n --upload-pack='touch ...;git-upload-pack' -- `; git parses and runs the second token. Impact: ACE (marker created, verified end-to-end). Bypass Evidence Live-verified on HEAD (tag 3.1.53): `_option_candidates` returns key-only candidate `['-n']`; `transform_kwargs` emits the smuggled `--upload-pack=` token; clone_from with the payload created the marker file; the direct-name `upload_pack=` form raised UnsafeOptionError. All prior bypasses (GHSA-rpm5 underscore key, GHSA-2f96 long-option abbreviation, GHSA-v396 joined short option, GHSA-x2qx multi-before-split) are BLOCKED on HEAD — this is a distinct kwarg-value->separate-token vector. Affected Versions `<= 3.1.53` Suggested Fix Make `_option_candidates` also emit candidates derived from single-character kwarg VALUES when `split_single_char_options` is in effect, OR run `check_unsafe_options` over the fully-transformed argv rather than the reconstructed name-only candidate list. --- Reported by zx (Jace) — GitHub: @manus-use

Related

Weakness · CWE (2)

  • CWE-78 Improper Neutralization of Special Elements used in an OS Command weakness
  • CWE-88 Improper Neutralization of Argument Delimiters in a Command weakness

References