{"schema":"threatline.record/2","record":{"id":"GHSA-f9j4-qggq-h239","aliases":["GHSA-f9j4-qggq-h239"],"summary":"GitPython Remote.pull() refspec bypasses the --upload-pack unsafe-option guard, enabling arbitrary command execution","details":"## Summary\r\n\r\n`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 `unsafe_git_pull_options` 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 (`allow_unsafe_options=False`).\r\n\r\n## Root Cause\r\n\r\n`Remote.pull()` is documented to reject dangerous options unless `allow_unsafe_options=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._option_candidates([], kwargs)`, passing an empty list in place of the positionals, so the `refspec` value is never compared against `unsafe_git_pull_options = [\"--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.\r\n\r\n```text\r\nAffected function: git.remote.Remote.pull\r\nSource: git/remote.py (guard at the check_unsafe_options call using _option_candidates([], kwargs); sink at the self.repo.git.pull(\"--\", self, refspec, ...) invocation)\r\n```\r\n\r\n## Impact\r\n\r\nAn 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.\r\n\r\n## Proof of Concept\r\n\r\n```text\r\nEnvironment: GitPython from the affected source tree; system git 2.43.0.\r\nThe pulled origin uses a local filesystem transport (protocol.file.allow=always for git >= 2.38).\r\n```\r\n\r\nReproduction steps:\r\n\r\n1. Create an origin repository reachable over a local-path transport.\r\n2. In a consumer repository, register that origin and call `Remote.pull()` with an attacker-controlled refspec.\r\n3. Observe that the injected command executes.\r\n\r\n```python\r\nimport git\r\nrepo = git.Repo(\"consumer\")                 # has an origin using a local-path or ssh transport\r\norigin = repo.remote(\"origin\")\r\n# Attacker-controlled refspec value; default arguments (allow_unsafe_options=False):\r\norigin.pull(refspec=\"--upload-pack=touch /tmp/pwned; git-upload-pack\")\r\n```\r\n\r\n```text\r\ncmdline: git pull -v -- origin --upload-pack=touch /tmp/pwned; git-upload-pack\r\n[attack]  marker created (RCE)      : True\r\n[isolate] upload_pack passed as KWARG blocked : True   (only the positional refspec escapes)\r\n[sibling] same value via git fetch  : safe            (pull-specific behavior)\r\n[control] benign refspec            : safe\r\n```\r\n\r\n## Attack Chain\r\n\r\n1. **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.\r\n2. **Control:** The attacker supplies the string `--upload-pack=<command>; git-upload-pack`. The value passes through `Git._unpack_args`, which only flattens and stringifies it, preserving the option shape.\r\n3. **Path:** The protocol loop calls `Git.check_unsafe_protocols` on the value, which only matches remote-helper transport URLs (a `<transport>::` form) and does not flag an `--upload-pack=` token.\r\n4. **Guard:** The unsafe-option guard calls `check_unsafe_options(options=Git._option_candidates([], kwargs), ...)`. Because positionals are passed as an empty list, the positional `refspec` is never inspected against `unsafe_git_pull_options`.\r\n5. **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.\r\n6. **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.\r\n\r\n## Bypass Evidence\r\n\r\nThe relevant guard is `Remote.pull()`'s own `check_unsafe_options` call, present in the current released artifact. It is incomplete: it inspects `Git._option_candidates([], kwargs)`, which only considers keyword arguments, so the positional `refspec` is never checked. This was confirmed empirically: passing the identical value as the `upload_pack=` keyword argument raises `UnsafeOptionError`, while passing it as the positional `refspec` does not, proving that only the positional escapes the guard.\r\n\r\nThe `--` 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 `check_unsafe_protocols` 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.\r\n\r\n## Affected Versions\r\n\r\n- `Ecosystem: pip (PyPI)`\r\n- `Package: GitPython`\r\n- `Confirmed affected range: >= 3.1.30`\r\n- `Latest release checked: 3.1.62 (PyPI); source verified against the 3.1.62 release tag`\r\n- `Fix status: not fixed in any released version or on the development branch`\r\n\r\nThe 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 `unsafe_git_pull_options` 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.\r\n\r\n## Suggested Fix\r\n\r\nEnforce 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._option_candidates` alongside `kwargs`) so that an option-shaped `refspec` such as `--upload-pack=...` is rejected with `UnsafeOptionError` unless `allow_unsafe_options=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.\r\n\r\nReported by zx (GitHub: @manus-pi).\r\n","categories":["Unclassified"],"product":"GitPython","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-f9j4-qggq-h239","source_modified_at":"2026-09-30T18:20:05Z","fetched_at":"2026-10-25T21:40:15.971031Z","package":{"ecosystem":"pip","name":"GitPython"},"range":">= 3.1.30"}],"fixed_versions":[],"cvss":9.8,"severity":"Critical","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-09-30T09:11:36Z","modified_at":"2026-09-30T18:20:05Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/gitpython-developers/GitPython/security/advisories/GHSA-f9j4-qggq-h239","fetched_at":"2026-10-25T21:40:15.971031Z","source_modified_at":"2026-09-30T18:20:05Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-09-30T18:20:05Z","content_hash":"9b338ab85d27961b6c99438b911a7055c27df7b767de7b5992b119ced0557c7e"}}
