Feb 26, 2026·6 min read·5 visits
Storybook's dev server left its WebSocket door wide open. If a developer visits a malicious site while Storybook is running, the site can hijack the connection (CSWSH), write malicious files to the local disk, and trigger RCE. Patch immediately to versions 7.6.23+, 8.6.17+, 9.1.19+, or 10.2.10+.
A critical flaw in the Storybook development server allows attackers to hijack the WebSocket connection from a malicious website via Cross-Site WebSocket Hijacking (CSWSH). Because the server failed to validate the `Origin` header or require authentication, a drive-by attack can silently connect to a developer's local instance, overwrite files, and achieve Remote Code Execution (RCE) on the developer's machine.
We often treat localhost like a sanctuary. It's the safe space where code is born, broken, and fixed before it faces the cruel judgment of production. But here's the dirty secret of modern web development: your local dev server is often more exposed than your production environment. Why? Because developers prize convenience over security. "It's just running locally, who can touch it?" turns out to be a very expensive assumption.
Enter Storybook, the industry standard for UI component development. Under the hood, the Storybook Dev Server runs a WebSocket channel (/storybook-server-channel) to synchronize the Manager (the UI you click) with the Preview (the iframe rendering your components). This channel is the nervous system of the application. It handles hot module reloading, event emission, and—crucially—file system operations like creating and saving stories.
Now, imagine if that nervous system had no immune system. No antibodies. No skin. Just raw nerves exposed to the open internet. That is exactly what CVE-2026-27148 is. It’s a mechanism that allows an attacker to reach into your localhost from their website and tell your Storybook server to start rewriting your project files.
The vulnerability here is a classic Cross-Site WebSocket Hijacking (CSWSH) issue. To understand it, you have to understand how browsers handle WebSockets. When a browser initiates a WebSocket handshake, it sends an HTTP GET request with an Upgrade: websocket header. Crucially, it also sends an Origin header, telling the server which site is asking for the connection. For standard HTTP requests (fetch/XHR), the browser enforces the Same-Origin Policy (SOP). But for WebSockets? The browser relies on the server to check that Origin header and say "No."
Storybook didn't say no. In fact, it didn't even look. The ServerChannelTransport implementation blindly accepted any incoming connection to /storybook-server-channel. It didn't care if the request came from http://localhost:6006 (the legit UI) or https://evil-hacker-blog.com.
This creates a terrifyingly simple attack vector. I don't need to be on your network. I don't need to phish your credentials. I just need you to visit my website while you have Storybook running in a background tab. My JavaScript runs in your browser, sees localhost (which your browser can reach), and opens a socket. Your browser attaches cookies (if there were any, though Storybook didn't use them here) and the connection is established. Game on.
Let's look at the anatomy of the failure. The vulnerability resided in how the server handled the upgrade event. In the vulnerable versions, the code looked effectively like this:
// VULNERABLE CODE (Conceptual)
server.on('upgrade', (request, socket, head) => {
if (request.url === '/storybook-server-channel') {
// "Come on in, the water's fine!"
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
}
});See the lack of logic? If the URL matches, the connection is granted. There is no check for the Origin header, and no token verification.
Now, let's look at the fix introduced in commit 54689a8add18ea75d628c540f4bc677592a1e685. The maintainers introduced a token-based authentication system. When the dev server starts, it generates a UUID. The frontend must present this UUID to connect.
// PATCHED CODE (Simplified)
const SERVER_CHANNEL_TOKEN = randomUUID(); // Generated at startup
server.on('upgrade', (request, socket, head) => {
const url = new URL(request.url, 'http://localhost');
if (url.pathname === '/storybook-server-channel') {
// The check that saves the day
const requestToken = url.searchParams.get('token');
if (!isValidToken(requestToken, SERVER_CHANNEL_TOKEN)) {
// "You shall not pass!"
socket.write('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request);
});
}
});This simple check neutralizes the attack. An external website cannot guess the random UUID generated when you started your server.
So I have a socket. What can I do with it? This is where it gets dark. The Storybook channel isn't just for chatting; it's an RPC mechanism. It supports event handlers for CREATE_STORY and SAVE_STORY. These handlers take a file path and content, and write it to the disk.
Here is the attack chain:
SAVE_STORY event.{
"type": "SAVE_STORY",
"args": [
{
"importPath": "./stories/Button.stories.ts",
"content": "import { exec } from 'child_process'; exec('calc.exe'); // ... rest of story code"
}
]
}Button.stories.ts.This is effectively RCE. If I can write to your filesystem and force a recompile, I own your shell.
The mitigation is straightforward: Update. The Storybook team has released patches for all supported major versions.
7.6.238.6.179.1.1910.2.10If you are stuck on an older version and cannot upgrade immediately (we've all been there), you have limited options. You could try to run Storybook behind a reverse proxy that strips the Origin header or enforces authentication, but honestly, that's more work than just upgrading package.json.
> [!WARNING]
> Do not use ngrok or similar tunneling services to share your local Storybook unless you are absolutely certain you have patched. Exposing a vulnerable dev server to the public internet turns a "Drive-By" attack into a "Public Invitation" for anyone scanning for open WebSockets.
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:A/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Storybook Storybook.js | < 7.6.23 | 7.6.23 |
Storybook Storybook.js | 8.1.0 - < 8.6.17 | 8.6.17 |
Storybook Storybook.js | 9.0.0 - < 9.1.19 | 9.1.19 |
Storybook Storybook.js | 10.0.0 - < 10.2.10 | 10.2.10 |
| Attribute | Detail |
|---|---|
| Attack Vector | Network (Drive-By) |
| CVSS v4.0 | 8.9 (High) |
| CWE | CWE-74 / CWE-79 |
| Impact | RCE / Persistent XSS |
| Exploit Status | PoC Available |
| Component | ServerChannelTransport |
Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')