GitPython versions up to 3.1.57 contain a vulnerability in the config-name validator that allows injection of arbitrary git configuration directives via crafted option names. The validator fails to reject characters such as '=', '#', ';', '[', ']', and whitespace in option names, enabling attackers to forge config directives like core.sshCommand or core.hooksPath. This can lead to remote code execution on subsequent git operations if the application forwards attacker-controlled option names to the config writer.
## Summary
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 `split_single_char_options` is True.
## Root Cause
`_option_candidates` derives value-token candidates only under `if len(key)==1 and split_single_char_options:` (cmd.py:1048, added by `e8d0fbf7`). With `split_single_char_options=False`, `_option_candidates([], {"n":"utouch <cmd>;git-upload-pack"})` returns only `['-n']` (not on the denylist), so the guard passes. But `transform_kwarg('n', value, split_single_char_options=False)` emits the JOINED token `-nutouch <cmd>;git-upload-pack` (cmd.py:1631). git clusters value-less short flags then parses `-u<cmd>` = `--upload-pack=<cmd>` → command execution. The hardened guard WOULD block the joined token if it saw it — the flaw is it never receives it.
## Impact
Arbitrary OS command execution as the host process (via `--upload-pack`) at default `allow_unsafe_options=False`, affecting all guarded methods that forward kwargs. Precondition: the app forwards a user-controlled kwargs dict containing `split_single_char_options=False` plus a single-char key (same user-dict-forwarding model GHSA-r9mr-m37c-5fr3 accepts).
## Proof of Concept
```python
from git import Repo
Repo.clone_from(src, dst,
n="utouch /tmp/ACE;git-upload-pack",
split_single_char_options=False) # /tmp/ACE created -> ACE
```
## Attack Chain
1. Entry: app forwards user kwargs to `Repo.clone_from(url, path, **kwargs)`: `{split_single_char_options: False, n: 'utouch /tmp/ACE;git-upload-pack'}`.
2. Check: `check_unsafe_options(_option_candidates([], kwargs), unsafe_git_clone_options)`. Guard: denylist includes `--upload-pack`/`-u`. Bypass proof: `_option_candidates` yields only `['-n']` (value token skipped because `split=False`); guard never sees `-u`.
3. Sink: `transform_kwarg` emits joined token (cmd.py:1631). argv (observed): `['git','clone','-v','-nutouch /tmp/ACE;git-upload-pack','--','<src>','<dst>']`.
4. Impact: git clusters `-n` + `-u<cmd>` → runs upload-pack command → ACE.
## Bypass Evidence
Independently reproduced (gate harness, default `allow_unsafe_options=False`): the `split=False` payload created the marker `VH05_GATE_ACE` (ACE); the clone returned normally (guard bypassed). Control: `n='--upload-pack=…'` (split default True) → `UnsafeOptionError: --upload-pack is not allowed`. Fix-commit read: `e8d0fbf7` extends candidates only under `if len(key)==1 and split_single_char_options:` — split=False skips value emission. Also confirmed the earlier clustering-parse fix (commit `56806080`) does not cover this because the guard only ever receives `['-n']`.
## Affected Versions
`GitPython <= 3.1.57` (code present verbatim on the latest release tag).
## Suggested Fix
Make `_option_candidates` emit value-derived candidates regardless of `split_single_char_options` (i.e. also for the joined `-n<value>` form), 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
## Summary
`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 received a guard and needs an independent fix.
## Root Cause
`Repo.init(path, mkdir, odbt, expand_vars, **kwargs)` is a bare `git.init(**kwargs)` (git/repo/base.py:1435) with no `check_unsafe_options` and no `allow_unsafe_options`.
## Impact
Arbitrary code execution (hook fires on next git op) at the privileges of the host process. Two preconditions raise attack complexity (AC:H): the app must forward a `template=` kwarg (KEY control) AND the attacker must stage an executable hook directory at a known path — the same profile GHSA-6p8h-3wgx-97gf accepted as HIGH for the clone path. Default `allow_unsafe_options` is irrelevant here because `Repo.init` has no guard at all.
## Proof of Concept
```python
# attacker stages /evil/hooks/post-commit (executable)
from git import Repo
Repo.init(path, template="/evil")
# next commit runs /evil/hooks/post-commit -> ACE
```
## Attack Chain
1. Entry: attacker stages `/evil/hooks/post-commit` (executable) and gets the app to call `Repo.init(path, template='/evil')`.
2. Check: NONE on `Repo.init`. Bypass proof: base.py:1435 is a bare `git.init(**kwargs)`. argv (observed): `['git','init','--template=/evil']`.
3. Sink: git copies `/evil/hooks/post-commit` → `<repo>/.git/hooks/post-commit`.
4. Impact: next commit runs the hook → arbitrary code execution.
## Bypass Evidence
Independently reproduced (gate harness): `Repo.init(dst, template='<evil>')` → argv `['git','init','--template=<evil>']` unguarded; hook copied into `.git/hooks/post-commit`; after `git commit` the `INIT_ACE` marker was created. `--separate-git-dir=<path>` is a parallel arbitrary-redirect vector through the same unguarded sink (value control only).
## Affected Versions
`GitPython <= 3.1.57` (unguarded `git.init(**kwargs)` present verbatim on the latest release tag).
## Suggested Fix
Add a `check_unsafe_options` guard (with an `allow_unsafe_options` parameter) to `Repo.init`, consulting a denylist that includes `--template` and `--separate-git-dir` (path-taking / hook-installing options).
---
Reported by **zx (Jace)** — GitHub: @manus-use
## Summary
`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](https://github.com/advisories/GHSA-3f7w-8rr8-f37f) - `TagReference.create()`
with `-F`, arbitrary file read returned in band - at two sites that advisory assessed and
cleared.
## Prior art, and why I am filing rather than commenting
GHSA-3f7w-8rr8-f37f's sweep table lists these four sites with the assessment
*"`--pathspec-from-file` only reads a pathspec; no write or disclosure primitive found"*:
| Call site | git command | that advisory's assessment |
|---|---|---|
| `IndexFile.remove()` | `rm` | `--pathspec-from-file` only reads a pathspec; no write or disclosure primitive found |
| `IndexFile.move()` | `mv` | same |
| `HEAD.reset()` | `reset` | same |
| `HEAD.checkout()` | `checkout` | same |
That assessment is very nearly right, and I think that is why it held: with
`--pathspec-from-file` alone, Git splits on newlines and the error quotes only the **first
line**, which reads as an uninteresting partial. Adding `--pathspec-file-nul` - a sibling flag
of the same option, and the documented way to handle paths containing newlines - makes the
whole file one pathspec.
## Root cause
`git/index/base.py:991-1043`:
```python
def remove(self, items, working_tree=False, **kwargs):
...
removed_paths = self.repo.git.rm(args, paths, **kwargs).splitlines() # line 1043
```
`git/refs/head.py:237-268`:
```python
def checkout(self, force: bool = False, **kwargs: Any):
...
self.repo.git.checkout(self, **kwargs) # line 268
```
Neither has an `allow_unsafe_options` parameter or a `check_unsafe_options()` call.
## Proof of concept
```python
from git import Repo
from git.exc import GitCommandError
repo = Repo("/path/to/repo")
kw = dict(pathspec_from_file="/etc/passwd", pathspec_file_nul=True)
try:
repo.index.remove([], **kw) # or: repo.heads[0].checkout(**kw)
except GitCommandError as e:
print(e.stderr) # <- entire file contents
```
Observed on published 3.1.57, against a canary file holding three marked lines:
```
[PASS] IndexFile.remove() -> `git rm` returns ALL 3 canary lines in-band
stderr: 'fatal: pathspec 'LINE1-CANARY-4242
LINE2-SECRET-7777
LINE3-TAIL-9999
' did not match any files'
[PASS] Head.checkout() -> `git checkout` returns ALL 3 canary lines in-band
stderr: 'error: pathspec 'LINE1-CANARY-4242
LINE2-SECRET-7777
LINE3-TAIL-9999
' did not match any file(s) known to git'
[PASS] PRECISION: `git status` leaks 0/3 -- not every unguarded site discloses
[PASS] PRECISION: the GUARDED checkout-index leaks 0/3
```
The two precision controls are there so the result is about these sinks and not about the
canary being visible everywhere.
## Scope correction to the table above
Of the four sites cleared with that sentence, **two disclose and two do not**:
| Call site | disclosed? |
|---|---|
| `IndexFile.remove()` → `git rm` | **yes, full file** |
| `Head.checkout()` → `git checkout` | **yes, full file** |
| `HEAD.reset()` → `git reset` | no - `git reset` does not error on unmatched pathspecs |
| `IndexFile.move()` → `git mv` | no |
The two negatives are mentioned because "the dismissal was wrong" would overstate it: the
dismissal was wrong for half of what it covered.
## Summary
`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` and `tag`; `read_tree` was left unprotected (it is among the acknowledged unguarded call sites in that advisory's sweep but was never reported or fixed).
## Root Cause
`from_tree` (index/base.py:388), `reset` (delegates to from_tree), and `merge_tree` (index/base.py:291) call `repo.git.read_tree(*arg_list)` with no `check_unsafe_options` and no `--`. The treeish is caller-influenced and positional.
## Impact
Arbitrary file overwrite / destruction at the privileges of the host process. Content is constrained to a git-index blob (not attacker-chosen, so not RCE), but the target path is fully attacker-controlled — corrupting/truncating configs or destroying files at attacker-chosen writable locations = I:H + A:H (per the skill's "overwrite-any-path = I:H" rule). Pure VALUE control (positional treeish). Default configuration.
## Proof of Concept
```python
IndexFile.from_tree(repo, "--index-output=/home/victim/.bashrc")
# target overwritten with a valid git-index blob (DIRC...)
```
## Attack Chain
1. Entry: app calls `IndexFile.from_tree(repo, treeish)` / `reset(commit=…)` / `merge_tree(base=…, rhs=…)` with attacker `treeish="--index-output=/home/victim/.bashrc"`.
2. Check: NONE — the methods have no `allow_unsafe_options` and never call `check_unsafe_options`.
3. Sink: `repo.git.read_tree(*arg_list)` — no `--`. argv (from_tree, observed): `['git','read-tree','--index-output=<tmp>','--index-output=/…/victim']` (last-wins).
4. Impact: target path created/overwritten with a valid git-index blob; existing content destroyed.
## Bypass Evidence
Independently reproduced (gate harness): `IndexFile.from_tree(repo,'--index-output=<victim>')` → victim overwritten; before=`IMPORTANT ORIGINAL CONTENT`, after starts `DIRC\x00\x00\x00\x02…` (destructive clobber, valid index blob). `reset(commit=…)` and both `merge_tree` positionals verified. Fix-commit read: `3af0c251` touched only `checkout_index`+`tag`; `read_tree` untouched on HEAD.
## Affected Versions
`GitPython <= 3.1.57` (sinks present verbatim on the latest release tag).
## Suggested Fix
Add a `check_unsafe_options` guard (with an `allow_unsafe_options` parameter) to `from_tree`/`reset`/`merge_tree`, and/or place a `--` separator before the positional treeish arguments; block `--index-output` (a path-taking option) on this sink.
Renovate versions 39.53.0 up to but not including 40.33.0 have a command injection vulnerability in the gleam manager. The depName parameter is not properly sanitized, allowing an attacker with repository write access to execute arbitrary commands on the host running Renovate by crafting malicious gleam.toml files. This vulnerability has a high severity with a CVSS score of 8.4.
Renovate versions from 31.51.0 up to but not including 40.33.0 have a command injection vulnerability in the helmv3 manager. This vulnerability arises because the repository parameter is appended to helm registry login commands without proper sanitization. An attacker with repository write access can exploit this by crafting malicious Chart.yaml files to execute arbitrary commands on the machine running Renovate. The vulnerability has a high severity score of 8.4.
Renovate versions from 32.135.0 up to but not including 40.33.0 contain a command injection vulnerability in the hermit manager component. This flaw allows attackers with repository write access to execute arbitrary commands by exploiting improper sanitization of malicious dependency names. The vulnerability has a CVSS 4.0 score of 8.4, indicating high severity.
Renovate versions from 35.63.0 up to but not including 40.33.0 contain a command injection vulnerability in the npm manager. This vulnerability allows attackers with repository write access to craft malicious Renovate configuration files that execute arbitrary commands on the host system due to improper sanitization of packageName values in npm install commands. The flaw has a high severity with a CVSS 4.0 score of 8.4.
Renovate versions from 39.218.0 up to but not including 40.33.0 contain a command injection vulnerability in the kustomize manager. This vulnerability allows attackers with repository write access to execute arbitrary commands on the Renovate host by leveraging malicious kustomization.yaml files with specially crafted chart names. The flaw arises because these chart names are appended unsafely to helm pull commands. The issue has a high severity score of 8.4.
CVE-2026-76228 is a command injection vulnerability in renovatebot's Renovate tool affecting versions >=32.124.0 and <42.68.5, and Mend renovate-ce/renovate-ee versions <13.3.0. The flaw occurs in the handling of Gradle Wrapper artifacts, where a malicious gradle-wrapper.properties file with a crafted distributionUrl can cause arbitrary command execution during Renovate's Gradle Wrapper update process. Exploitation requires an attacker to introduce this malicious file into a repository scanned by Renovate. This vulnerability is rated high severity with a CVSS score of 8.4. No patch links or vendor advisory details are provided, so patch status is not confirmed.
CVE-2026-76227 is a vulnerability in renovatebot's renovate tool where environment variables are not restricted when spawning child processes. This allows child processes to access all environment variables of the parent Renovate process, potentially exposing sensitive information. The issue affects multiple renovate versions and Docker images prior to specific fixed versions. The vulnerability has a medium severity score of 6.8 CVSS v4.0.
Renovate versions from 43.65.0 up to but not including 43.102.11 contain a remote code execution vulnerability in the bazel-module and bazelisk managers when using lockFileMaintenance. This vulnerability allows attackers with high privileges to execute arbitrary code by supplying malicious dependencies referenced in bazel mod deps calls, including within ctx.execute statements. The vulnerability has a medium severity score of 6.8 CVSS v4.0.
Renovate versions from 37.158.0 up to but not including 37.199.0 have a command injection vulnerability in the helmv3 manager's registryAliases handling. Attackers with commit access can inject arbitrary OS commands by exploiting unquoted shell metacharacters in registryAliases keys. These commands execute during helm repo add operations, potentially allowing full access to Renovate's execution environment. The vulnerability has a high severity score of 8.4.
GitPython versions prior to 4.0.4_7 contain a vulnerability where an attacker-controlled submodule name in a cloned repository's .gitmodules file can cause GitPython to create and initialize a Git repository outside the intended working directory. This occurs because GitPython does not validate the submodule name, allowing directory traversal sequences that escape the clone directory. The vulnerability requires the victim to clone a malicious repository and run submodule initialization via GitPython. Core Git itself mitigates this attack, but GitPython's independent implementation lacks this check.