Vitest APIサーバーのWebSocketからテストファイルを書き換えてRCEに届くCVE-2025-24964
目次
TL;DR
影響 Vitestの api オプション、またはVitest UIを使っている環境。対象は <=0.0.125、1.0.0 以上 1.6.1 未満、2.0.0 以上 2.1.9 未満、3.0.0 以上 3.0.5 未満
起きること ブラウザで悪意あるページを開いたとき、ローカルで待ち受けているVitest APIのWebSocketへ接続され、テストファイル書き換え→再実行
対応 1系は 1.6.1 以上、2系は 2.1.9 以上、3系は 3.0.5 以上へ更新。このCVEはLAN公開がなくてもlocalhostで成立するので、更新が最優先
VitestのCVE-2025-24964は、テストランナーの便利機能がそのまま開発者端末のRCE(リモートコード実行)経路になるタイプの脆弱性だ。
vitest --ui や api オプションでVitest APIサーバーが立っている状態で、開発者が悪意あるWebページを開く。
そのページから ws://localhost:51204/__vitest_api__ のようなローカルWebSocketへ接続され、Vitest側のAPIを叩かれる。
脆弱な版では、このWebSocketサーバーが Origin ヘッダー(接続元のスキーム・ホスト・ポートを示す値)を検証せず、認可の仕組みも持っていなかった。
ブラウザの同一オリジンポリシーはWebSocket接続を完全には止めない。WebSocketはハンドシェイク時にサーバー側が Origin を検証する責任があり、そこを省くと別サイトからローカルの待ち受け口へ届く。
この手口はCSWSH(Cross-Site WebSocket Hijacking)と呼ばれる。
saveTestFile と rerun が組み合わさる
Vitest APIにはテストファイルを保存する saveTestFile と、指定ファイルのテストを再実行する rerun がある。
どちらもUIからテストを編集・再実行するには自然な機能だ。
でも、外部サイトからWebSocket APIを呼べる状態だと、攻撃者はVitestが認識している既存のテストファイルへ任意のJavaScriptを書き込み、そのファイルをVitestに実行させられる。
GitHub advisoryのPoC(概念実証コード)はWindowsの calc 起動で示しているが、見る場所は電卓ではない。
テストはNode.jsプロセスとして開発者の権限で走るので、環境変数、リポジトリ内の設定ファイル、SSH鍵やクラウドCLIの認証情報に触れる経路ができる。
CIではなくローカル開発端末で起きるRCEとして見るほうが実態に近い。
flowchart TD
A["開発者がVitest UI<br/>またはapi有効化で起動"] --> B["localhostに<br/>Vitest API WebSocket"]
C["悪意あるWebページを開く"] --> D["ブラウザから<br/>localhostのAPIへ接続"]
D --> E["saveTestFileで<br/>テストを書き換え"]
E --> F["rerunで<br/>書き換えたテストを実行"]
F --> G["開発者権限で<br/>Node.jsコード実行"]
style G fill:#991b1b,color:#fff
攻撃にユーザー操作が要る点は見落としやすい。
ネットワーク越しにVitestへ直接1リクエスト投げれば終わるRCEではなく、「Vitest APIが待ち受けている」「開発者が攻撃ページを開く」「ブラウザからlocalhostへ届く」という並びになる。
それでもCVSS(共通脆弱性評価システム)のスコアは高い。採番機関であるGitHub(CNA)の評価は9.6でCritical、NVD(NIST)の評価は8.8でHighと、評価元で割れている。一部の二次データベースは9.7と表示するが、一次情報の値ではない。
修正版は系統ごとに違う
修正版は3系だけ見れば終わりではない。
GitHub advisoryの修正済みバージョンは、1系、2系、3系で分かれている。
| 系統 | 脆弱な範囲 | 修正版 |
|---|---|---|
| 0系 | <=0.0.125 | 直接の継続系統ではなく、現行系へ移行 |
| 1系 | 1.0.0 以上 1.6.1 未満 | 1.6.1 以上 |
| 2系 | 2.0.0 以上 2.1.9 未満 | 2.1.9 以上 |
| 3系 | 3.0.0 以上 3.0.5 未満 | 3.0.5 以上 |
手元のプロジェクトが対象かどうかは、package.json の範囲指定ではなくロックファイルの解決版で確認する。
pnpm why vitest
pnpm list vitest
npmなら npm ls vitest、Yarnなら yarn why vitest で見る。
CIでだけVitestを使っているプロジェクトでも、開発者ローカルで vitest --ui を起動する運用があるなら同じ確認対象になる。
Browser ModeのCVE-2025-24963とは入口が違う
同じ日に、VitestのGHSA-8gvc-j273-4wm5 / CVE-2025-24963も出ている。
こちらはRCEではなく、Browser ModeのHTTPサーバーにある __screenshot-error ハンドラが任意ファイルを返す問題だ。
browser.api.host: true でサーバーをネットワークへ明示的に出している場合、リモートからファイル内容を読まれる。
2つのCVEは同じVitestの開発用サーバーまわりだが、踏み方は別だ。
| CVE | 対象機能 | 入口 | 影響 |
|---|---|---|---|
| CVE-2025-24964 | API WebSocket | 悪意あるWebページからlocalhost WebSocketへ接続 | テストファイル書き換え経由のRCE |
| CVE-2025-24963 | Browser Mode HTTP server | browser.api.host: true などで外部からHTTP到達 | 任意ファイル読み取り |
CVE-2025-24963の修正版は2系が 2.1.9 以上、3系が 3.0.4 以上。
一方でCVE-2025-24964まで含めて見るなら、3系は 3.0.5 以上が必要になる。
どちらか片方だけを見て 3.0.4 で止めると、RCE側が残る。
開発サーバーをLANに出す設定を疑う
Vitest公式ドキュメントの現行ページでは、設定項目として api と browser.api が分かれている。
通常のローカル作業なら、これらをLANや0.0.0.0へ出す理由はあまりない。
モバイル実機、別マシンのブラウザ、コンテナ外からの接続を通すためにhost設定を広げたプロジェクトは、今回のような開発サーバー系CVEで急に外から到達できる口が増える。
見る場所は vitest.config.ts、vite.config.ts、package.jsonの scripts、CIの起動コマンドだ。
--ui、api、browser.api.host、host: true、0.0.0.0、固定ポートの指定があるか確認する。
Dockerやdevcontainerで「外から見えるlocalhost」を作っている場合も、ブラウザから見える経路とコンテナ内の待ち受けがどうつながるかを追う。
開発者ツールのローカル到達という意味では、以前書いたVS Code Remote-SSHで侵害済みSSH先からローカル端末へコマンドが届く話に近い。
あちらはRemote-SSHの信頼モデル、今回はVitest APIのOrigin検証不足だが、どちらも「開発中だけの便利な口」が手元の権限へ届く。
npmパッケージ全体の防御としては、pnpm 11のminimumReleaseAgeとYarn/npmのage gateも別の防御層にある。
ただし、今回のCVEは悪意ある新規パッケージを踏む話ではなく、既に入っているVitestの脆弱版を起動したときの話だ。
ロックファイル固定、依存更新、開発サーバーの待ち受け先確認はそれぞれ別の層になる。