## Summary
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.
## Root Cause
`base.py:145-152` defines `unsafe_git_clone_options = ["--upload-pack","-u","--config","-c"]` — `--template` is absent. The guard candidate `['--template']` passes `check_unsafe_options` (verified). git copies the hook directory and executes `post-checkout` at checkout time. git's `protocol.allow`/`GIT_ALLOW_PROTOCOL` do not gate `--template`; the incomplete denylist is the only defense.
## Impact
Arbitrary OS command execution during clone (default config). Requires an attacker-readable directory containing an executable hook — a genuine second precondition (realistic via shared filesystems, upload dirs, `/tmp`, or attacker-writable network paths), reflected as AC:H.
## Proof of Concept
```python
# attacker stages <dir>/hooks/post-checkout (chmod +x)
from git import Repo
Repo.clone_from(src, dst, template='<dir>') # post-checkout hook executes -> marker created (verified)
```
## Attack Chain
1. Setup: attacker stages `<dir>/hooks/post-checkout` (chmod +x). Guard: n/a (filesystem).
2. Entry: `Repo.clone_from(url, path, template='<dir>')`. Guard: `check_unsafe_options(candidates=['--template'], unsafe=unsafe_git_clone_options)`. Bypass proof: `--template` not on the denylist -> passes (verified candidate `['--template']`, no error).
3. Sink: git copies the hook and executes `post-checkout` at checkout. Impact: ACE, default config (verified marker created).
## Bypass Evidence
Live-verified on HEAD (tag 3.1.53): guard candidate `['--template']` passed with no error; staged `post-checkout` hook executed during `clone_from`, creating the marker. Independent of the value-smuggle bypass (`--template` is a legitimate long option that survives any single-char-value fix). Not covered by any existing advisory.
## Affected Versions
`<= 3.1.53`
## Suggested Fix
Add `--template` (and audit for other hook/exec-influencing options) to `unsafe_git_clone_options`.
---
Reported by **zx (Jace)** — GitHub: @manus-use
**Target:** gitpython-developers/GitPython
**Tested:** HEAD `07e80555` (2026-07-25), latest release 3.1.55, `git version 2.50.1`
**Reported instances:** 2 exploitable, from a sweep of 14 unguarded call sites
## Summary
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:
| # | Call site | git option | Impact |
|---|---|---|---|
| 1 | `IndexFile.checkout()` → `git checkout-index` | `--prefix=<path>` | arbitrary file **overwrite** with repository-controlled content |
| 2 | `TagReference.create()` → `git tag` | `-F <file>` / `--file=<file>` | arbitrary file **read**, returned in-band |
This is the same defect class already fixed in `Commit.count()` (GHSA-p538-c434-8v24), `Repo.archive()` and `Git.ls_remote()` (GHSA-956x-8gvw-wg5v). Both instances below are still present at HEAD.
---
## Instance 1 — `IndexFile.checkout()`: arbitrary file overwrite
`git/index/base.py:1210` accepts `**kwargs` and forwards them with no guard:
```python
def checkout(self, paths=None, force=False, fprogress=lambda *args: None, **kwargs):
...
proc = self.repo.git.checkout_index(*args, **kwargs) # line 1331
...
proc = self.repo.git.checkout_index(args, **kwargs) # line 1349
```
There is no `allow_unsafe_options` parameter and no `check_unsafe_options()` call in the method.
`git checkout-index` accepts `--prefix=<string>`, prepended to every output path. It is not confined to the working tree, so an absolute prefix writes tracked file contents anywhere the process can write, and `-f` overwrites what is already there.
### Reproduction
```python
from git import Repo
Repo("/path/to/repo").index.checkout(prefix="/tmp/target_dir/", a=True, f=True)
```
Observed (`poc/poc_checkout_index.py`) — no exception raised, files land outside the repository:
```
[ALLOWED] no UnsafeOptionError raised
files written outside the repo: ['f.txt']
f.txt: 'hi\n'
```
Overwrite of a pre-existing file (`poc/poc_ci_overwrite.py`) — the victim file held `ORIGINAL-DO-NOT-CLOBBER\n` before the call:
```
[ALLOWED] no exception
victim content now: 'hi\n'
OVERWRITTEN: True
```
### Why this rates High
Both halves of the write are attacker-influenced:
- **Destination** — the `prefix` kwarg.
- **Content** — the bytes written are repository blobs, so anyone who can land a file in the repository (a pull-request branch, a mirrored or untrusted repository, an agent-cloned repository) controls exactly what is written.
Commit a file named `authorized_keys`, `.bashrc`, `config` or `post-checkout`, choose the matching prefix (`~/.ssh/`, `~/`, `.git/hooks/`), and the write becomes code execution as the service account.
For comparison within this project: GHSA-fjr4-x663-mwxc (arbitrary file overwrite via `git diff --output`) is rated High, and GHSA-p538-c434-8v24 (arbitrary file *truncation* via `git rev-list --output`) is rated Medium. `--prefix` supplies full content control, so it sits at or above the former.
---
## Instance 2 — `TagReference.create()`: arbitrary file read
`git/refs/tag.py:88` forwards `**kwargs` into `git tag` with no guard, and the signature advertises the passthrough:
```python
def create(cls, repo, path, reference="HEAD", logmsg=None, force=False, **kwargs):
"""...
:param kwargs:
Additional keyword arguments to be passed to :manpage:`git-tag(1)`.
"""
```
`git tag` accepts `-F <file>` / `--file=<file>`, which reads the tag message from an arbitrary path. The annotated tag object stores that content and GitPython returns it to the caller via `TagReference.tag.message`, so the file contents come back in-band.
### Reproduction
```python
from git import Repo
from git.refs.tag import TagReference
t = TagReference.create(Repo("/path/to/repo"), "x", force=True, a=True, F="/etc/passwd")
print(t.tag.message)
```
Observed (`poc/poc_tag_F.py`), reading a canary file outside the repository:
```
[ALLOWED] no UnsafeOptionError raised
>>> tag message recovered from arbitrary path: 'TAG-READ-CANARY-98765\nsecond-line-secret'
```
Impact is a read at the privileges of the process. I am not claiming code execution for this instance. The signing options (`-s`, `-u`/`--local-user`) do invoke gpg from the same unguarded kwargs, but I did not develop that into command execution and make no claim about it.
---
## Sweep results — the other 12 sites
Reported so the fix can be scoped once rather than per report. `poc/sweep.py` reproduces this list.
| Call site | git command | Assessment |
|---|---|---|
| `IndexFile.from_tree()` | `read-tree` | `--index-output=<path>` looked reachable but i
**Target:** gitpython-developers/GitPython
**Tested:** HEAD `07e80555` (2026-07-25), latest release 3.1.55, `git version 2.50.1`
## Summary
`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**.
```python
# git/repo/base.py:169
unsafe_git_archive_options = [
# Allows arbitrary command execution through the remote git-upload-archive command.
"--exec",
# Writes output to a caller-controlled filesystem path.
"--output",
"-o",
]
```
The comment on `--output` states the protected class in the project's own words: an option that lets the caller name **a filesystem path** is unsafe. `--output` is blocked because it *writes* to a caller-chosen path.
`git archive` also accepts `--add-file=<path>` and `--add-virtual-file=<path:content>` (both present in current git; verified against `git version 2.50.1`). `--add-file` *reads* a caller-chosen path — including an absolute path outside the repository — and places the bytes into the archive the caller receives. Neither option is in the list, and no other layer references them:
```
$ grep -rniE "add.file|add_file" git/
git/index/base.py:771: R"""Add files from the working tree, ... # unrelated docstring
```
Net effect: the guard blocks arbitrary file **write** at this sink while permitting arbitrary file **read** at the same sink.
## Reachability proof (verified at the sink)
`poc/poc_addfile.py` at HEAD `07e80555`. The PoC creates its own out-of-tree canary, so it runs from a clean machine:
```
-- CONTROL: options the denylist covers (expect BLOCKED) --
[BLOCKED] output='/tmp/gp_written.tar': --output is not allowed, use `allow_unsafe_options=True` to allow it.
[BLOCKED] o='/tmp/gp_written.tar': -o is not allowed, use `allow_unsafe_options=True` to allow it.
[BLOCKED] exec='touch /tmp/gp_exec': --exec is not allowed, use `allow_unsafe_options=True` to allow it.
-- SIBLING OMITTED FROM THE DENYLIST: --add-file (expect ALLOWED) --
[ALLOWED] add_file='/tmp/gp_canary.txt' -> archive 10240 bytes
archive members: ['f.txt', 'gp_canary.txt']
>>> EXFILTRATED gp_canary.txt: 'secret-canary-12345'
>>> byte-for-byte match with the out-of-tree file: CONFIRMED
-- also: --add-virtual-file (attacker-chosen name AND content) --
[ALLOWED] add_virtual_file='pwn.txt:hello' -> archive 10240 bytes
```
The three blocked lines are the control: they prove the guard is active on this call path, so the fourth result is a gap in list membership rather than a guard that never ran.
Minimal reproduction:
```python
import io, tarfile
from git import Repo
buf = io.BytesIO()
Repo("/path/to/repo").archive(buf, format="tar", add_file="/etc/passwd")
print(tarfile.open(fileobj=io.BytesIO(buf.getvalue())).getnames())
# ['<repo files>', 'passwd'] <- contents readable by whoever receives the archive
```
The canary is untracked and lives outside the repository; its contents are recovered from the returned archive and asserted byte-for-byte against the on-disk file. The option is rendered by `transform_kwargs` into `--add-file=<path>` and reaches `git archive` unmodified.
## Direct precedent
`GHSA-6p8h-3wgx-97gf` (High, published 2026-07-22) is the same defect on the sibling list: *"Incomplete `unsafe_git_clone_options` denylist omits `--template`"* — an option absent from one of these denylists, reachable under the same caller-controlled-options precondition, accepted and fixed by adding it. `git log` shows the archive list itself has already been extended reactively once, in `701ce32f` (*fix: Guard unsafe git command options*, GHSA-956x-8gvw-wg5v), and the `--template` omission was then fixed separately in `ffcb5359`.
## `--add-virtual-file` is the same gap pointing the other way
`--add-virtual-file=<path:content>` lets the caller inject **attacker-chosen content under an attacker-chosen name** into an archive that downstream consumers will reasonably treat as repository-derived.
## Suggested remediation
1. **Preferred — allowlist.** `Repo.archive()` has a small legitimate option surface (`format`, `prefix`, `worktree_attributes`, `remote`, compression level, plus paths). Accepting those and rejecting the rest means a future git release cannot add another path-taking option that silently reopens this.
2. **Minimum — extend the list** with `--add-file` and `--add-virtual-file`, and make the membership rule *"the option takes a filesystem path or URL"* rather than *"the option executes a command"*. The existing comment on `--output` already implies that rule; applying it consistently is what closes the class instead of this instance.
## Scope limits
- Impact is **arbitrary file read at the privileges of the process**. Not code execution — I make no such claim here.
- It requires the embedding application to forward caller-influenced kwargs into `Repo.archive()`. That is the identical precondition to `--output`, `--exec` and `--template`,
yt-dlp versions from 2.10.5 up to but not including 2.10.5_5 do not restrict file extensions for downloaded files, allowing creation of arbitrary filenames including executables. Because yt-dlp reads configuration files from the working directory and executes Windows executables found there, this can lead to arbitrary code execution. The issue is fixed in yt-dlp version 2024.07.01 by whitelisting allowed extensions. Users are advised to upgrade and follow recommended safe usage practices to mitigate risk.
yt-dlp versions from 2.10.5 up to but not including 2.10.5_7 included DouyuTV and DouyuShow extractors that fetched a component of the crypto-js JavaScript library from a third-party CDN (cdn.bootcdn.net) controlled by a known malicious actor. This JavaScript code was executed externally using PhantomJS, potentially exposing users to malicious code execution if certain conditions were met. The issue is fixed in yt-dlp version 2024.07.07 by removing the malicious CDN URL and invalidating cached data.
yt-dlp's --exec option is vulnerable to arbitrary command injection when unsafe string formatting conversions are used in the command template. This allows an attacker to execute arbitrary shell commands via maliciously crafted video metadata. The vulnerability affects users who use unsafe conversions like %()s in --exec templates. yt-dlp 2026.06.09 fixes this by restricting allowed conversions to safe ones and limiting characters in templates. Users can mitigate by upgrading or avoiding unsafe conversions or the --exec option altogether.
A Unicode normalization mismatch in setuptools' handling of MANIFEST.in exclusions can cause files intended to be excluded from source distributions to be included instead. This occurs on macOS APFS/HFS+ filesystems where filenames are stored in NFD form but MANIFEST.in patterns are typically in NFC form, leading to silent exclusion bypasses. This can result in unintended disclosure of sensitive files when publishing to PyPI.
A path traversal vulnerability in setuptools' PackageIndex.download function allows an attacker to write files to arbitrary filesystem locations with the permissions of the running process. The flaw arises from insufficient sanitization of URL-derived filenames used in os.path.join, enabling path traversal despite some replacement of '..' sequences. This vulnerability was fixed in setuptools version 1.17.0_1. Exploitation could lead to arbitrary file write and potentially remote code execution depending on context. The affected component is deprecated, reducing the attack surface.
Webpack versions from 5.49.0 up to but not including 5.104.0 have a vulnerability in the HttpUriPlugin when experiments.buildHttp is enabled. The plugin enforces allowedUris only on the initial URL but fails to re-validate after HTTP redirects, allowing bypass of the allow-list. This can lead to build-time SSRF and inclusion of untrusted content in build outputs. The issue is fixed in version 5.104.0.
Webpack versions from 5.49.0 up to but not including 5.104.1 have a vulnerability in the HTTP(S) resolver when experiments.buildHttp is enabled. This flaw allows bypassing the allowedUris allow-list by using URLs with userinfo (username:password@host), leading to build-time SSRF and untrusted content inclusion. The issue is patched in version 5.104.1.
CVE-2025-66034 is an arbitrary file write vulnerability in the fontTools.varLib component used by WeasyPrint. It arises from unsanitized filename handling and XML injection in .designspace files, allowing attackers to write files to arbitrary filesystem locations and inject malicious content. This can lead to remote code execution without elevated privileges and potential privilege escalation by overwriting critical system files. The vulnerability affects fontTools.varLib versions >=61.0 and <66.0_2 and has been fixed in version 66.0_2.
Scrapy versions up to 2.13.3 are vulnerable to a denial of service (DoS) attack due to a flaw in the brotli decompression implementation. The existing protection against decompression bombs does not mitigate the brotli variant, allowing remote servers to cause clients to crash by consuming excessive memory during decompression. This vulnerability is fixed in version 66.0_1 of Weasyprint.
The Vite development server option `server.fs.deny` can be bypassed on case-insensitive filesystems, such as Windows, by using case-augmented filenames. This allows an attacker to access files that are intended to be blocked by the deny list. The issue arises because the underlying glob matching is case-sensitive, while the filesystem is not, enabling blacklist bypass. This vulnerability affects Vite versions from 2.7.0 up to but not including 5.0.12 and has been fixed in 5.0.12 and earlier patch releases of prior major versions.
Vite's development server option `server.fs.deny` did not properly deny requests for patterns that include directories, such as `/foo/**/*`. This issue affects applications that customize `server.fs.deny` with directory patterns and expose the Vite dev server to the network. The vulnerability arises from the use of picomatch with the `matchBase: true` option, which only matches basenames and not full paths, allowing unauthorized access to files under denied directories like `.git`. This flaw was fixed in multiple Vite versions starting with 2.9.18, 3.2.10, 4.5.3, 5.0.13, 5.1.7, and 5.2.6.
Vite versions from 2.5.1 up to but not including 5.4.6 contain a vulnerability where the server's file system access restrictions can be bypassed using the '?import&raw' query parameters. This allows an attacker to retrieve the contents of arbitrary files outside the intended serving allow list. The vulnerability has a medium severity with a CVSS score of 5.3.
A DOM Clobbering vulnerability in Vite affects bundled scripts built with output formats cjs, iife, or umd. This flaw allows an attacker to manipulate the document.currentScript property by injecting a scriptless HTML element named 'currentScript', causing the application to load attacker-controlled scripts dynamically. This can lead to cross-site scripting (XSS) in web pages that include vulnerable Vite bundles and allow embedding of such HTML elements. The vulnerability impacts Vite versions from 2.5.1 up to but not including 5.4.6. A fix is available.
Vite development server versions from 2.5.1 up to but not including 6.0.9 have a vulnerability that allows any website to send requests to the dev server and read responses due to permissive default CORS settings and lack of Origin header validation on WebSocket connections. This affects even local-only dev servers. The vulnerability can expose sensitive development files to malicious websites. A fix is available in newer Vite versions. Mitigations include configuring CORS origins, allowed hosts, and using HTTPS or updated browsers. Some mitigations are not possible without upgrading.
A vulnerability in Vite allows bypassing the server.fs.deny restriction when using the ?raw?? or ?import&raw?? query parameters, enabling access to arbitrary file contents. This affects Vite dev servers explicitly exposed to the network via the --host option or server.host configuration. The issue arises because trailing separators in the query string are removed inconsistently, allowing unauthorized file reads outside the allowed serving paths. A proof of concept demonstrates reading sensitive files such as /tmp/secret.txt. The vulnerability affects Vite versions from 2.5.1 up to but not including 6.2.3. A patch is available to address this issue. The CVSS score is 5.3 (medium severity).
Vite versions from 2.5.1 up to but not including 6.2.5 have a vulnerability that allows arbitrary file contents to be returned to the browser. This occurs when the Vite development server is exposed to the network using the --host option or server.host configuration. The vulnerability involves bypassing the server.fs.deny restriction via requests ending with .svg or using relative paths before normalization. The issue affects files smaller than the default 4kB inline asset limit in Vite 6.0 and later. Exploitation requires network access to the dev server and user interaction (UI:R).
Vite versions from 2.5.1 up to but not including 6.2.6 have a vulnerability in the dev server's file system access control. When running on Node or Bun runtimes and explicitly exposed to the network, specially crafted HTTP requests with an invalid request-target containing a '#' character can bypass the server.fs.deny check. This allows an attacker to retrieve arbitrary files from the server. The vulnerability does not affect Vite running on Deno due to different URL handling. A proof-of-concept demonstrates reading sensitive files like /etc/passwd via this bypass.