GHSA-f9j4-qggq-h239: OS Command Injection

Published Oct 27, 2026
·
Updated

Summary

Remote.pull() in GitPython forwards its positional refspec argument directly to the git pull child process while its unsafe-option guard inspects only keyword arguments. An application that passes attacker-influenced data as the refspec (for example a user-supplied branch or refspec parameter in a web or CI service) can supply an option-shaped value such as --upload-pack=<command>. The value bypasses the unsafegitpulloptions check, is placed after the -- separator on the git pull command line, and is executed as the upload-pack program for local-path or SSH transports. This yields arbitrary command execution with the privileges of the calling process using the documented default arguments (allowunsafeoptions=False).

Root Cause

Remote.pull() is documented to reject dangerous options unless allowunsafeoptions=True, and --upload-pack is explicitly listed as such an option. The guard, however, only screens keyword arguments and never the positional refspec. The check builds its candidate list with Git.optioncandidates([], kwargs), passing an empty list in place of the positionals, so the refspec value is never compared against unsafegitpulloptions = ["--upload-pack"]. The normalized refspec is then forwarded verbatim to the git pull invocation. The leading -- separator that neutralizes an option-shaped refspec for git fetch and git push does not protect git pull, because git pull re-forwards its trailing positionals to an internal git fetch without re-inserting a -- separator, so the positional becomes an option to that internal fetch.

text Affected function: git.remote.Remote.pull Source: git/remote.py (guard at the checkunsafeoptions call using optioncandidates([], kwargs); sink at the self.repo.git.pull("--", self, refspec, ...) invocation)

Impact

An attacker who controls the refspec value passed to Remote.pull() achieves arbitrary operating-system command execution as the process invoking GitPython, with no additional privileges and no user interaction, on the default code path. Exploitation requires that the pulled remote uses a transport that runs upload-pack on the local side, namely a local filesystem path or SSH; over HTTPS the --upload-pack value is inert because the server ignores a client-supplied upload-pack program. This transport requirement narrows the population of vulnerable deployments but does not add work for the attacker once a vulnerable target is reached. Working checkouts commonly use local-path or SSH origins, so the exposed population is substantial.

Proof of Concept

text Environment: GitPython from the affected source tree; system git 2.43.0. The pulled origin uses a local filesystem transport (protocol.file.allow=always for git >= 2.38).

Reproduction steps:

1. Create an origin repository reachable over a local-path transport. 2. In a consumer repository, register that origin and call Remote.pull() with an attacker-controlled refspec. 3. Observe that the injected command executes.

python 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 (allowunsafeoptions=False): origin.pull(refspec="--upload-pack=touch /tmp/pwned; git-upload-pack")

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

Attack Chain

1. Exposure: An application calls Remote.pull(refspec=<value>) on the default path, where <value> is derived from attacker-influenced input such as a branch or refspec request parameter. 2. Control: The attacker supplies the string --upload-pack=<command>; git-upload-pack. The value passes through Git.unpackargs, which only flattens and stringifies it, preserving the option shape. 3. Path: The protocol loop calls Git.checkunsafeprotocols on the value, which only matches remote-helper transport URLs (a <transport>:: form) and does not flag an --upload-pack= token. 4. Guard: The unsafe-option guard calls checkunsafeoptions(options=Git.optioncandidates([], kwargs), ...). Because positionals are passed as an empty list, the positional refspec is never inspected against unsafegitpulloptions. 5. Primitive: The value is forwarded to git pull -v -- <remote> --upload-pack=<command>; git-upload-pack; git pull re-forwards the positional to an internal git fetch without a -- separator, so it is honored as the upload-pack program. 6. Result: For a local-path or SSH transport, git executes <command> as the upload-pack program, giving arbitrary command execution as the calling process user.

Bypass Evidence

The relevant guard is Remote.pull()'s own checkunsafeoptions call, present in the current released artifact. It is incomplete: it inspects Git.optioncandidates([], kwargs), which only considers keyword arguments, so the positional refspec is never checked. This was confirmed empirically: passing the identical value as the uploadpack= keyword argument raises UnsafeOptionError, while passing it as the positional refspec does not, proving that only the positional escapes the guard.

The -- separator on the git pull command line was tested as a possible mitigation and refuted: with system git 2.43.0, git pull -- <remote> '--upload-pack=<cmd>' executed the injected command, while the identical value supplied to git fetch -- ... and git push -- ... did not execute, because those subcommands honor the -- terminator whereas git pull re-forwards positionals to an internal fetch without one. The checkunsafeprotocols transport check was also examined and does not flag the --upload-pack= token because it has no <transport>:: form. No prior published advisory for this package addresses Remote.pull, the fetch or pull refspec positional, or --upload-pack injection via a refspec; the closest accepted issue covers the same class (a positional argument escaping a keyword-only guard) in a different function and sink.

Affected Versions

- Ecosystem: pip (PyPI) - Package: GitPython - Confirmed affected range: >= 3.1.30 - Latest release checked: 3.1.62 (PyPI); source verified against the 3.1.62 release tag - Fix status: not fixed in any released version or on the development branch

The keyword-only pull guard together with the positional refspec sink is present in every released tag from 3.1.30 through the latest release 3.1.62, verified by inspecting the shipped git/remote.py in each tagged artifact. Version 3.1.30 introduced the unsafegitpulloptions guard; the affected range is stated from that boundary under the incomplete-guard framing. Releases before 3.1.30 forward the refspec positionally with no unsafe-option guard and no -- separator and are also exploitable, but the range is stated conservatively from the guard-introduction boundary. No fixed release exists.

Suggested Fix

Enforce the unsafe-option invariant on the positional refspec, not only on keyword arguments. The guard should build its candidate list from the actual forwarded positionals (for example by passing the normalized refspec list into Git.optioncandidates alongside kwargs) so that an option-shaped refspec such as --upload-pack=... is rejected with UnsafeOptionError unless allowunsafeoptions=True. The same correction should be verified for any other reference or refspec forwarding path that relies on the keyword-only guard. As an interim mitigation without a code change, callers should validate or reject refspec values that begin with -, avoid passing untrusted data as the refspec, and prefer HTTPS remotes where a client-supplied upload-pack program is inert.

Reported by zx (GitHub: @manus-pi).

Affected Software

1 affected componentFixes available
pip/gitpython>=3.1.30<3.2.0
3.2.0

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade pip/GitPython to a version that resolves this vulnerability.

    Fixed in 3.2.0
  2. Compensating control

    Update Remote.pull() so its unsafe-option guard includes the normalized positional refspec in Git._option_candidates alongside kwargs, causing option-shaped values such as --upload-pack=... to be rejected with UnsafeOptionError unless allow_unsafe_options=True.

  3. Compensating control

    As an interim mitigation, reject refspec values that begin with '-' and do not pass attacker-controlled data as refspec; prefer HTTPS remotes, where a client-supplied upload-pack program is inert.

Event History

Oct 27, 2026
Advisory Published
via GitHub·03:26 PM
Data Sourced
via GitHub·03:26 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

Are applications using the documented default settings affected?

Yes. The issue is exploitable with allow_unsafe_options=False because the unsafe-option check does not inspect the positional refspec argument.

2

Which deployments are realistically exposed?

Applications that pass attacker-influenced branch names or refspecs to Remote.pull() are exposed, such as web or CI services that accept user-supplied repository references. Exploitation applies when the pull uses a local-path or SSH transport.

3

What must an attacker be able to control?

The attacker needs control of the positional refspec supplied to Remote.pull(). An option-shaped value such as --upload-pack=<command> can bypass the keyword-only unsafe-option guard and execute a command with the calling process's privileges.

4

Does placing the value after the git pull -- separator prevent exploitation?

No. The affected behavior places the refspec after --, but Git still processes the option-shaped value as the upload-pack program for the affected transports.

5

What can be done before an update is available?

Do not pass untrusted input as the positional refspec to Remote.pull(). In particular, prevent user-controlled values from being supplied where they could take the form of a Git option.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203