Skip to main content

Free 30-min security demo Book Now

Offensive360 Offensive360
ZeroDays CVE-2026-67325
High CVE-2026-67325 CVSS 8.8 GitPython Python

GitPython Option Prefix Command Injection

CVE-2026-67325: GitPython's incomplete blocklist allows attackers to bypass unsafe-option guards via abbreviated git long-options, enabling arbitrary command execution.

Offensive360 Research Team
Affects: < 3.1.51
Source Code

Overview

CVE-2026-67325 is a command injection vulnerability in GitPython, the widely-used Python library for interacting programmatically with Git repositories. The flaw resides in the library’s “unsafe options” guard — a blocklist mechanism intended to prevent callers from passing dangerous Git options such as --upload-pack or --receive-pack to Git subcommands. Critically, the blocklist performs exact-string matching against known dangerous option names but fails to account for Git’s long-option prefix abbreviation feature, which allows any unambiguous prefix of a long option name to be silently resolved to the full option at runtime.

The vulnerability was identified by security researchers and disclosed through the GitPython project’s GitHub Security Advisory program. It affects all releases of GitPython prior to 3.1.51 and carries a CVSS v3.1 score of 8.8 (High), reflecting the low attack complexity and the significant privileges an attacker can obtain. Any application that exposes GitPython’s clone, fetch, or ls-remote interfaces to user-controlled input — a common pattern in CI/CD platforms, repository hosting services, and developer tooling — is directly at risk.

The vulnerability is a variant of a well-documented problem class: incomplete denylist validation. Rather than being a straightforward bypass of a missing check, the subtlety here lies in Git’s own CLI design, which treats abbreviated options as first-class citizens. This makes the bypass invisible to string-comparison defenses and easy to miss in both manual code review and naive static analysis configurations.

Technical Analysis

GitPython’s Git class constructs shell-level Git invocations from Python method calls, translating keyword arguments into command-line flags. To prevent abuse of options that can redirect Git’s internal protocol commands (and therefore execute arbitrary binaries), the library maintains a set of known dangerous option names:

# Simplified representation of the pre-3.1.51 blocklist logic
UNSAFE_OPTIONS = {
    "--upload-pack",
    "--receive-pack",
    "--upload-archive",
    "--exec",
    # ... other known-dangerous options
}

def _check_unsafe_options(options: list[str]) -> None:
    for option in options:
        # Exact-match check — only catches the full canonical option name
        if option in UNSAFE_OPTIONS:
            raise UnsafeOptionError(
                f"Option '{option}' is not allowed for security reasons."
            )

The fundamental problem is the option in UNSAFE_OPTIONS membership test. Git’s option parser, however, accepts any unambiguous prefix of a long option. The following invocation is semantically identical to git clone --upload-pack=/usr/bin/evil from Git’s perspective:

# Git resolves --upload_p (and other unambiguous prefixes) to --upload-pack
git clone --upload_p=/usr/bin/evil https://example.com/repo target/

Because "--upload_p" is not a member of UNSAFE_OPTIONS, the Python-level guard passes without raising an exception, and the full Git command — including the attacker-controlled binary path — is executed by the subprocess layer.

Note also the use of underscores: GitPython normalises Python keyword arguments by converting underscores to hyphens when building the CLI invocation, but an attacker supplying a raw option string (rather than a keyword argument) can mix hyphens and underscores in ways that further confuse naïve string matching while remaining acceptable to Git’s parser.

A realistic exploitation payload passed through an application’s “custom git options” interface would look like:

import git

# Attacker controls `extra_options`; application passes it through to GitPython
extra_options = {"upload_p": "/path/to/attacker-controlled-binary"}

# GitPython builds: git clone --upload_p=/path/to/attacker-controlled-binary ...
repo = git.Repo.clone_from(
    url="https://legitimate-looking-host.example/repo.git",
    to_path="/tmp/cloned",
    **extra_options,
)

The root cause maps to CWE-184 (Incomplete List of Disallowed Inputs) compounded by a failure to understand the semantics of the external process being invoked. The blocklist was written against a mental model of Git’s option parser that does not match its actual behaviour.

Impact

An attacker who can influence the options passed to GitPython’s Git-invocation layer can execute an arbitrary binary on the host with the privileges of the process running GitPython. The CVSS 8.8 score reflects a network-accessible attack surface (AV:N), no required privileges in the common deployment scenario (PR:N), low attack complexity (AC:L), and full integrity and confidentiality impact on the affected component.

Concrete consequences include:

  • Remote Code Execution on CI/CD runners, build servers, or any backend that clones or fetches user-supplied repositories.
  • Credential exfiltration by substituting a credential-harvesting binary for upload-pack, capturing Git credentials passed over the internal protocol.
  • Lateral movement in environments where the Git process user has access to secrets, cloud metadata endpoints, or internal network segments.
  • Supply chain compromise in platforms that perform automated dependency resolution or mirror operations via GitPython.

Any service that accepts a repository URL or Git options from end users and processes them with GitPython prior to 3.1.51 should be treated as compromised until patched and audited.

How to Fix It

Upgrade immediately. GitPython 3.1.51 resolves the bypass by switching from an exact-string blocklist to prefix-aware matching that mirrors Git’s own option resolution semantics.

# pip
pip install "gitpython>=3.1.51"

# poetry
poetry add "gitpython>=3.1.51"

# pipenv
pipenv install "gitpython>=3.1.51"

# uv
uv add "gitpython>=3.1.51"

The corrected validation logic expands each candidate option against all known dangerous full-form option names before allowing it through:

# Representative of the fixed approach in 3.1.51+
import re

UNSAFE_OPTIONS_FULL = [
    "--upload-pack",
    "--receive-pack",
    "--upload-archive",
    "--exec",
]

def _is_prefix_of_unsafe_option(option: str) -> bool:
    """Return True if `option` is an unambiguous prefix of any unsafe option."""
    # Strip leading dashes and normalise underscores to hyphens
    normalised = re.sub(r"^-+", "", option).replace("_", "-")
    candidate = f"--{normalised}"
    matches = [
        full for full in UNSAFE_OPTIONS_FULL
        if full.startswith(candidate)
    ]
    return len(matches) >= 1  # Any prefix match is rejected

def _check_unsafe_options(options: list[str]) -> None:
    for option in options:
        opt_name = option.split("=")[0]  # Strip any =value suffix
        if opt_name in UNSAFE_OPTIONS_FULL or _is_prefix_of_unsafe_option(opt_name):
            raise UnsafeOptionError(
                f"Option '{option}' is not allowed for security reasons."
            )

Beyond upgrading, defenders should audit application code for any path that accepts user-supplied strings and forwards them to GitPython without sanitisation, and enforce an allowlist of permitted options rather than relying solely on a blocklist.

Our Take

This vulnerability is a textbook example of the gap between what a developer thinks a blocklist covers and what the downstream interpreter actually accepts. The GitPython maintainers correctly identified the class of dangerous options and wrote a guard — but modelled Git’s parser incorrectly. This pattern recurs across the industry wherever security controls are layered on top of external processes without a precise understanding of those processes’ input grammars.

For enterprises, the lesson is architectural: blocklists are inherently fragile. Allowlists, subprocess argument arrays (avoiding shell interpretation entirely), and strict option whitelisting are far more durable controls. When a library wraps an external binary as complex as Git, the attack surface is the entire Git option namespace — which evolves with every Git release and contains many options with security implications beyond the obvious ones.

From a SAST/DAST programme perspective, this vulnerability class is a reminder that tool configuration matters as much as tool selection. A scanner running default rules will catch os.system() calls but may not model the semantics of prefix abbreviation in a subprocess wrapper. Organisations need custom rules that flag any pass-through of user-controlled strings to subprocess-wrapped external binaries.

Detection with SAST

SAST detection of this vulnerability class falls under CWE-184 (Incomplete List of Disallowed Inputs) and CWE-88 (Improper Neutralization of Argument Delimiters in a Command). Offensive360’s analysis engine flags the following patterns:

  • Taint propagation into GitPython API surfaces: Any data flow where user-controlled input reaches git.Repo.clone_from(), git.Git().execute(), or analogous methods without passing through a validated allowlist.
  • Membership-test guards on subprocess option lists: String membership checks (in set(...), == literal) applied to options before subprocess invocation, where the checked set does not account for prefix equivalence or normalisation.
  • Underscore-to-hyphen normalisation gaps: Locations where option strings are transformed before subprocess execution but the security check is applied to the pre-transformation value.
  • kwargs forwarding patterns: Functions that accept **kwargs and forward them to Git-invoking methods, particularly when those kwargs originate from an HTTP request, configuration file, or other external input source.

In DAST, the approach is to fuzz the options layer of any endpoint that accepts repository metadata or custom Git parameters with a corpus of abbreviated and underscore-normalised variants of known-dangerous options, observing for out-of-band command execution via DNS or HTTP callbacks.

References

#command-injection #argument-injection #git #input-validation

Detect this vulnerability class in your codebase

Offensive360 SAST scans your source code for CVE-2026-67325-class vulnerabilities and thousands of other patterns — across 60+ languages.