October 22, 2026
Storybook CVE-2026–114125: From Cross-Origin Messaging to Code Injection
A missing origin check in Storybook’s preview transport allowed a malicious website to send crafted messages to a running Storybook…

By Ajith Prabhu
6 min read
A missing origin check in Storybook's preview transport allowed a malicious website to send crafted messages to a running Storybook development environment. By chaining this with the preview's highlight functionality and privileged server-channel handlers, I was able to create and overwrite project files, write outside the project root, and enumerate source files.
The vulnerability is tracked as CVE-2026–114125, published under GHSA-f2ph-cmg5-wxc8, with a Critical severity rating of CVSS 9.4.
What made this interesting was that the attacker did not need to connect directly to Storybook's privileged WebSocket endpoint. Instead, the attack used the legitimate preview running in the developer's browser as a bridge to the development server.
The Vulnerability
Storybook uses postMessage to communicate between its manager and preview. The preview also maintains a connection to the Storybook development server through a server-channel WebSocket.
While examining the preview's message transport, I found that it accepted incoming messages containing:
{
key: "storybook-channel",
event: { /* ... */ }
}{
key: "storybook-channel",
event: { /* ... */ }
}The preview transport in code/core/src/channels/postmessage/index.ts did not validate the origin of the incoming message before accepting it.
The manager-side implementation performed sender-origin validation, but the corresponding preview path did not enforce the same trust boundary.
This was the first problem: a message from an unrelated website could be accepted as a legitimate Storybook channel message.
The consequences became more significant when I traced how those messages were processed.
Tracing the Message to the Development Server
1. The preview has a privileged server-channel connection
Storybook's createBrowserChannel, implemented in code/core/src/channels/index.ts, establishes a WebSocket connection to the development server:
ws://<host>:<port>/storybook-server-channel?token=...ws://<host>:<port>/storybook-server-channel?token=...This connection is used for communication between the preview and the local development environment.
The important distinction is that the browser's preview already has a legitimate connection to the server. The attacker does not need to independently establish that connection or obtain its token.
If attacker-controlled input can cause the preview to emit arbitrary server-channel events, the preview effectively becomes a confused deputy: it uses its own authority to carry out actions initiated by another origin.
2. The highlight functionality provides the bridge
The next piece was in:
code/core/src/highlight/useHighlights.tscode/core/src/highlight/useHighlights.tsThe highlight functionality allows elements in the preview to be highlighted and associated with interactive menu items.
The menu renderer effectively performs the following operation:
channel.emit(event, id, getEventDetails(target));channel.emit(event, id, getEventDetails(target));Both the event name and the first argument can be influenced by the highlight payload.
This mattered because the preview was not merely accepting an untrusted message. It was allowing data from that message to influence which event would be emitted over its privileged server channel.
By crafting the highlight payload, I could reach server-side handlers that were never intended to be exposed through an arbitrary cross-origin message.
3. The server processes the emitted events
The server registers handlers for various operations, including:
saveStoryRequestfileComponentSearchRequestcreateNewStoryfileRequestghostStoriesopenInEditorRequest
These handlers perform actions against the developer's project or interact with local development tools.
The resulting attack chain was:
Attacker-controlled website
|
| postMessage
v
Storybook preview
|
| Unvalidated channel message
v
Highlight functionality
|
| Crafted event name and arguments
v
Privileged server-channel WebSocket
|
v
Storybook development server
|
+--> Create files
+--> Overwrite files
+--> Write outside project root
+--> Enumerate project sources
+--> Invoke editor-launch handlerAttacker-controlled website
|
| postMessage
v
Storybook preview
|
| Unvalidated channel message
v
Highlight functionality
|
| Crafted event name and arguments
v
Privileged server-channel WebSocket
|
v
Storybook development server
|
+--> Create files
+--> Overwrite files
+--> Write outside project root
+--> Enumerate project sources
+--> Invoke editor-launch handlerThe issue was therefore not simply missing origin validation in isolation. The impact came from combining that validation failure with a message-processing path that allowed attacker-controlled data to reach privileged server-side operations.
Reproducing the Issue
I reproduced the vulnerability against Storybook 10.6.0 using a minimal React/Vite Storybook project.
The test environment contained a basic component and story file:
src/
├── Button.tsx
└── Button.stories.tsxsrc/
├── Button.tsx
└── Button.stories.tsxI started the development server:
./node_modules/.bin/storybook dev -p 6155 --no-open --ci./node_modules/.bin/storybook dev -p 6155 --no-open --ciI then served a separate page from another origin, http://127.0.0.1:8099, and embedded the Storybook preview:
<iframe
src="http://127.0.0.1:6155/iframe.html?id=example-button--primary&viewMode=story">
</iframe><iframe
src="http://127.0.0.1:6155/iframe.html?id=example-button--primary&viewMode=story">
</iframe>In the tested configuration, the development server did not send X-Frame-Options or a CSP frame-ancestors policy that prevented this embedding.
From the foreign-origin page, I sent a crafted message to the iframe. The following is a shortened representation of the highlight payload used in the test:
{
"key": "storybook-channel",
"event": {
"type": "storybook/highlight/add",
"args": [
{
"id": "<hl>",
"selectors": ["button"],
"styles": {
"outline": "2px dashed #f0f"
},
"menu": [
{
"id": {
"id": "pwn-1",
"payload": {
"componentFilePath": "src/PWNED.tsx",
"componentExportName": "PWNED",
"componentIsDefaultExport": true,
"componentExportCount": 1
}
},
"title": "Open component docs",
"clickEvent": "createNewStoryfileRequest"
}
]
}
]
}
}{
"key": "storybook-channel",
"event": {
"type": "storybook/highlight/add",
"args": [
{
"id": "<hl>",
"selectors": ["button"],
"styles": {
"outline": "2px dashed #f0f"
},
"menu": [
{
"id": {
"id": "pwn-1",
"payload": {
"componentFilePath": "src/PWNED.tsx",
"componentExportName": "PWNED",
"componentIsDefaultExport": true,
"componentExportCount": 1
}
},
"title": "Open component docs",
"clickEvent": "createNewStoryfileRequest"
}
]
}
]
}
}The message creates a highlight with a menu item whose event name is attacker-controlled.
To activate the demonstrated path, I performed two genuine clicks inside the preview: one to open the menu associated with the highlighted element, and another to activate the menu item.
The preview then emitted a WebSocket message to the Storybook development server, and the server created:
src/PWNED.stories.tsxsrc/PWNED.stories.tsxThis confirmed that the cross-origin message could influence an operation performed by the local development server.
Why the interaction requirement matters
An important detail of this proof of concept is that the demonstrated attack required two genuine user clicks inside the preview.
Sending the same highlight payload without performing those clicks did not emit the relevant WebSocket event or create the file.
This distinction matters when assessing exploitability. The issue allowed a malicious origin to prepare the interaction, but the demonstrated file-write path was not interaction-free.
From File Creation to Path Traversal
Once I established that the server was processing attacker-controlled event names and arguments, I tested other reachable handlers.
Creating and overwriting project files
Using createNewStoryfileRequest, I could create a new story file.
Using saveStoryRequest, I could overwrite an existing story file, including:
src/Button.stories.tsxsrc/Button.stories.tsxThis was more serious than simply creating an unexpected file. An existing project file could be modified without the developer intentionally initiating the corresponding operation.
Writing outside the project root
The most notable result came from tracing the server-side implementation of save-story.ts.
The code used the attacker-controlled importPath when constructing the destination path:
join(process.cwd(), importPath)join(process.cwd(), importPath)In my test, I supplied:
../../../../tmp/csf-target.stories.tsx../../../../tmp/csf-target.stories.tsxThis resulted in a file being written to:
/tmp/csf-target.stories.tsx/tmp/csf-target.stories.tsxThe destination was outside the Storybook project root.
The underlying issue was that joining a path to the current working directory does not, by itself, guarantee that the resolved destination remains inside that directory. Traversal components such as .. can escape the intended location unless the application performs appropriate validation.
This expanded the impact from unauthorized modifications within the project to writes elsewhere on the filesystem, subject to the development server process's filesystem permissions.
Enumerating source files
I also reached fileComponentSearchRequest.
This allowed me to enumerate project source files and exported component names through the server's registered handler.
Although this operation did not itself modify files, it demonstrated that the attack could access information about the developer's project through a channel the attacker should not have been able to control.
Reaching the editor-launch handler
Finally, I tested openInEditorRequest with an attacker-controlled absolute path:
{
"file": "/etc/hostname",
"line": 1,
"column": 1
}{
"file": "/etc/hostname",
"line": 1,
"column": 1
}This reached the server-side editor-launch handler.
Reaching the handler is not, by itself, proof of arbitrary code execution. The ultimate effect depends on the implementation, editor configuration, and surrounding conditions. I treat it as evidence of access to another privileged operation rather than claiming that this particular test independently established code execution.
Impact
The vulnerability breaks the trust boundary between an arbitrary website and a locally running Storybook development environment.
My reproduction demonstrated the following:
- Unauthorized file creation: Create new story files in the project.
- File overwrite: Modify existing project files.
- Path traversal: Write files outside the project root.
- Source enumeration: Discover project files and exported component names.
- Editor-launch handler access: Reach a server-side operation with an attacker-controlled path.
The ability to modify source files is particularly concerning because Storybook projects commonly run development tooling that processes those files. Depending on the contents of a modified file and the developer's subsequent workflow, the changes could become a route to code execution with the developer's privileges.
However, the distinction between demonstrated impact and worst-case impact is important.
My reproduction established the message-to-server attack chain and the file operations described above. The broader public advisory describes a worst-case scenario in which unauthorized file changes, combined with additional conditions and vulnerabilities, could result in arbitrary code execution with the developer's privileges.
The public advisory also states that the maintainers were not aware of exploitation in the wild.
Affected Versions and Remediation
According to the GitHub Security Advisory, affected releases include:
- Storybook 8.x: Versions before
8.6.19→ Upgrade to8.6.19or later. - Storybook 9.x: Versions before
9.1.21→ Upgrade to9.1.21or later. - Storybook 10.x: Versions before
10.6.2→ Upgrade to10.6.2or later. - Storybook 11.x (alpha): Versions before
11.0.0-alpha.6→ Upgrade to11.0.0-alpha.6or later.
Other affected packages include @storybook/core, @storybook/core-server, @storybook/experimental-addon-test, @storybook/addon-vitest, and @storybook/addon-mcp
Check the advisory for the exact ranges applicable to your dependencies, and upgrade to a patched release before running the development server again.
If an immediate upgrade is not possible, avoid running an affected Storybook development server. Do not rely solely on browser protections or the fact that the server is bound to localhost.
Disclosure Timeline
- September 30, 2026 4:45 PM IST: The Storybook team accepted and acknowledged my vulnerability report.
- October 1, 2026 4:06 PM IST: The team identified that the worst-case scenario could lead to code execution, highlighting the potential severity of the vulnerability.
- October 13, 2026 12:43 AM IST: The security advisory was published, documenting the vulnerability and remediation.
- October 21, 2026 10:37 AM IST: GitHub assigned the vulnerability CVE-2026–114125.
- October 22, 2026: Write-up published