marimoの/terminal/ws未認証RCE(CVE-2026-39987)— 自分の環境が外に出ていないか確認する
リアクティブな Python ノートブック marimo の 0.23.0 より前のバージョンに、未認証のリモートコード実行がある。CVE-2026-39987、NVD の CVSS v4 基本値は 9.3(Critical)で、CISA の KEV(既知の悪用された脆弱性カタログ)にも収載されている。
原因は単純で、ターミナル用の WebSocket エンドポイント /terminal/ws に認証検証が無い。ネットワークから到達できる marimo サーバがあれば、認証なしでシェルに接続して任意のコマンドを実行できる。
効くのは「開発用だから」と公開している環境
この脆弱性が刺さるのは、本番サービスというより開発環境だ。典型的なのは次のパターン。
- クラウドVM上で
marimo edit --host 0.0.0.0して、ブラウザから触っている - 社内ネットワークの共有GPUマシンで動かしっぱなしにしている
- Docker で起動し、
-p 2718:2718のようにホスト側の全インターフェースへ公開している
いずれも「社内だから」「自分しか知らないポートだから」で運用されがちだが、認証が無い以上、到達できる誰か(または何か)がいれば終わりだ。
確認1:入っているバージョンを見る
まず自分の環境のバージョンを確認する。
1 | |
0.23.0 より前なら対象だ。仮想環境を複数持っている場合は、環境ごとに確認する必要がある。
確認2:待ち受けアドレスを見る
インストール状況より重要なのが「どこに向けて待ち受けているか」だ。実際に開いているポートを見る。
1 | |
出力の待ち受けアドレスが 0.0.0.0:2718 や *:2718 なら全インターフェースに公開されている。127.0.0.1:2718 なら、少なくともローカルホスト以外からは直接届かない。
Docker で動かしている場合は、コンテナ内の bind ではなくホスト側の公開設定を見る。
1 | |
確認3:エンドポイントが応答するか
到達性そのものを確認したい場合は、自分が管理している ホストに対して、そのポートが外から開いているかを見る。クラウドVMなら、セキュリティグループやファイアウォールの設定を先に確認するほうが早い。
1 | |
0.0.0.0/0 から該当ポートが空いているルールがあれば、そこが入口になる。
対処
正攻法は更新だ。
1 | |
修正は上流の PR #9098 とコミット c24d480 で入っており、GitHub Security Advisory は GHSA-2679-6mx9-h9xc として公開されている。0.23.0 以降に上げる。
すぐに更新できない場合の緩和策は次のとおり。
待ち受けを 127.0.0.1 に限定する。 リモートのVMで触りたい場合は、公開する代わりに SSH ポートフォワードを使う。
1
2# ローカルの 2718 をリモートの 127.0.0.1:2718 に転送
ssh -N -L 2718:127.0.0.1:2718 user@remote-hostファイアウォール/セキュリティグループで該当ポートを閉じる。 社内からのみ到達させたいなら、送信元IPを絞る。
リバースプロキシで認証を必須にする。 ただしこれは
/terminal/wsを含むすべてのパスに認証がかかっていることが前提で、WebSocket のアップグレードリクエストが認証をすり抜けない設定になっているかを確認する必要がある。
教訓としての一般化
「開発用のツールだから認証が無い」は、そのツールをローカルホスト以外に出した瞬間に成り立たなくなる。Jupyter がトークン認証を既定にしている理由もここにある。ノートブック系のツールを新しく導入するときは、機能を見る前に「既定の bind アドレスは何か」「認証は既定で有効か」の2点を確認しておくと、この種の事故をまとめて避けられる。
各クラウドでの対応状況は 脆弱性対応ウォッチ に CVE 単位でまとめている。