
marimo における CVE-2026-39987 を再現・理解するためのローカル専用 Docker ラボです。 このプロジェクトは、脆弱な marimo サービスとパッチ適用済みの marimo サービスを並べて比較し、最小限の害しか及ぼさない証明スクリプトを用いてその違いを示します。
CVE-2026-39987 は、リアクティブ Python ノートブックフレームワークである marimo における、重大な事前認証リモートコード実行の脆弱性です。
脆弱な挙動は、ターミナル WebSocket エンドポイントに存在します:
/terminal/ws
影響を受けるバージョンでは、このエンドポイントは有効な認証なしで到達可能であり、対話型ターミナルセッションを作成できます。脆弱な marimo 編集サーバーに到達できる認証されていない攻撃者は、marimo プロセスの権限でコマンドを実行できる可能性があります。
このラボは、管理されたローカル Docker 環境でこの脆弱性を再現します:
| サービス | バージョン | URL | 期待される挙動 |
|---|---|---|---|
vuln | marimo 0.20.4 | http://127.0.0.1:8081 | /terminal/ws が認証なしの WebSocket 接続を受け入れる |
patched | marimo 0.23.0 | http://127.0.0.1:8082 | /terminal/ws が認証なしの WebSocket 接続を 403 Forbidden で拒否する |
目的は、武器化されたエクスプロイトを提供することではありません。目的は、ローカル専用で再現可能な証拠を用いて、脆弱なバージョンとパッチ適用済みバージョンの間の観察可能なセキュリティ上の違いを示すことです。
影響を受けるコンポーネントは、marimo のターミナル WebSocket エンドポイントです:
/terminal/ws
このエンドポイントは、marimo の編集環境がブラウザ接続の WebSocket セッションを通じてターミナル機能を提供するために使用されます。
脆弱なエンドポイントは、保護された marimo 編集機能に期待されるものと同じ認証チェックを適用せずに WebSocket 接続を受け入れていました。
重要な違いは次のとおりです:
脆弱な挙動:
認証なしのクライアントが /terminal/ws に接続できる
ターミナルセッションが作成される
WebSocket を通じてコマンドを送信できる
パッチ適用済みの挙動:
認証なしのクライアントは拒否される
WebSocket ハンドシェイクが 403 Forbidden で失敗する
ターミナルセッションは作成されない
パッチは、ターミナルセッションの確立を許可する前に、ターミナル WebSocket フローに認証検証を追加します。
脆弱な marimo 編集サーバーが到達可能なネットワークに公開されている場合、認証されていない攻撃者が marimo プロセスを実行しているユーザーとしてコマンドを実行できる可能性があります。
このラボでは、marimo プロセスは意図的に低権限の非 root ユーザーとして実行されます:
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
これにより、脆弱性を証明しながらもデモの安全性が保たれます。
このラボは、次の3つのことを証明するように設計されています:
/terminal/ws への認証なしの WebSocket 接続を受け入れる。403 Forbidden で拒否する。このラボは、破壊的なコマンド、永続化、リバースシェル、資格情報のダンプ、またはインターネットに公開されたターゲットを意図的に避けています。
.
├── docker-compose.yml
├── vuln/
│ ├── Dockerfile
│ └── notebook.py
├── patched/
│ ├── Dockerfile
│ └── notebook.py
├── poc/
│ ├── poc.py
│ ├── rce_poc.py
│ └── requirements.txt
├── SAFETY.md
├── README.md
└── .gitignore
ホストマシン
127.0.0.1:8081 ─────► vuln コンテナ
marimo 0.20.4
/terminal/ws が認証なしの WebSocket を受け入れる
127.0.0.1:8082 ─────► patched コンテナ
marimo 0.23.0
/terminal/ws が認証なしの WebSocket を拒否する
両方のサービスは marimo の内部ポート 2718 を公開しますが、ホストのポートは異なります:
vuln -> 127.0.0.1:8081
patched -> 127.0.0.1:8082
サービスは 127.0.0.1 にのみバインドされています。LAN やインターネットに公開することを意図していません。
コンテナは、可能な限りいくつかのガードレールで構成されています:
- ポートを 127.0.0.1 のみにバインド
- 非 root ユーザーとして実行
- Linux ケーパビリティを削除
- no-new-privileges を有効化
- 読み取り専用のルートファイルシステムを使用
- 限定的な tmpfs 書き込み場所のみを提供
- 専用の Docker ブリッジネットワーク内でサービスを分離
これらの制御は、脆弱なサービスから脆弱性を除去するものではありません。ローカルデモの爆発半径を縮小します。
テスト済みの前提:
- Docker Desktop を備えた Linux x86_64 または macOS
- Docker Compose v2
- Python 3.9+
- ローカルホストのみでのテスト
必要なツール:
docker --version
docker compose version
python3 --version
プロジェクトディレクトリにクローンまたは移動します:
cd cve-2026-39987
両方のサービスをビルドして起動します:
docker compose up -d --build
両方のコンテナが実行されていることを確認します:
docker compose ps
期待されるサービス:
cve-2026-39987-vuln
cve-2026-39987-patched
脆弱なサービスのバージョンを確認します:
docker compose exec vuln marimo --version
期待される出力:
0.20.4
パッチ適用済みのサービスのバージョンを確認します:
docker compose exec patched marimo --version
期待される出力:
0.23.0
Python 仮想環境を作成します:
python3 -m venv .venv
source .venv/bin/activate
依存関係をインストールします:
python -m pip install -r poc/requirements.txt
主要な証明スクリプトは次のとおりです:
poc/poc.py
これは次の場所への接続を試みます:
/terminal/ws
次に、無害な証明コマンドのみを送信します:
id
whoami
hostname
リバースシェル、ファイル書き込み、永続化、資格情報へのアクセス、または破壊的なコマンドは使用されません。
実行:
python poc/poc.py --base-url http://127.0.0.1:8081
期待される結果:
[*] Target WebSocket: ws://127.0.0.1:8081/terminal/ws
[*] Sending benign proof command only: id; whoami; hostname
[+] websocket connected without credentials
[result] VULNERABLE: unauthenticated command execution observed
[proof]
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
スクリプトが生のターミナル出力を表示するが、次のブロックを含む場合、脆弱性は依然として確認されています:
CVE39987_PROOF_START
uid=10001(marimo) gid=10001(marimo) groups=10001(marimo)
marimo
<container-hostname>
CVE39987_PROOF_END
これは、WebSocket 接続が成功し、コマンド出力がターミナルセッションから返ってきたことを意味します。
実行:
python poc/poc.py --base-url http://127.0.0.1:8082
期待される結果:
[*] Target WebSocket: ws://127.0.0.1:8082/terminal/ws
[*] Sending benign proof command only: id; whoami; hostname
[-] websocket connection rejected/failed: Handshake status 403 Forbidden
[result] not exploitable by this unauthenticated check
これは、パッチ適用済みのサービスがターミナルセッションを作成する前に認証なしのアクセスを拒否することを示しています。
以下のファイルは、ローカルでの学習のみを目的としています:
poc/rce_poc.py
これは、脆弱なターミナル WebSocket に接続してターミナル操作を可能にすることで、同じ問題をよりリアルタイムに示します。
これはローカル Docker ラボに対してのみ使用してください。
python poc/rce_poc.py --base-url http://127.0.0.1:8081
期待される挙動:
[+] Connected successfully (Pre-Auth RCE)
✓ Interactive shell is ready to use!
試すことができる安全なコマンド:
id
whoami
hostname
pwd
python -c 'import marimo; print(marimo.__version__)'
exit
python poc/rce_poc.py --base-url http://127.0.0.1:8082
期待される挙動:
[-] Connection Failed: 403 Forbidden
[!] This is likely the PATCHED version.
The WebSocket endpoint is now protected.
リアルタイムクライアントは、脆弱な WebSocket がどのように動作するかを理解するのに役立つために存在します。このローカルラボ以外のシステムに対して使用しないでください。
ポートフォリオ公開用の推奨される主要な証拠は、依然として poc/poc.py です。これは、制限され、再現可能で、最小限の害しか及ぼさないためです。
ラボの主な結果は次のように要約されるべきです:
vuln / marimo 0.20.4:
認証なしの WebSocket ハンドシェイクが /terminal/ws に対して成功する
無害なコマンド出力が観察可能
patched / marimo 0.23.0:
認証なしの WebSocket ハンドシェイクが /terminal/ws に対して失敗する
サーバーが 403 Forbidden を返す
ターミナルセッションは作成されない
これが CVE 再現の核となる証明です。
動作を手動で検査したい場合は、WebSocket 対応クライアントを使用して次の場所に接続します:
ws://127.0.0.1:8081/terminal/ws
ws://127.0.0.1:8082/terminal/ws
期待される結果:
8081 -> 接続が受け入れられる
8082 -> 403 Forbidden
PoC スクリプトの方が明確な証拠を生成するため推奨されます。
実際の環境では、有用な指標には次のものが含まれます:
- /terminal/ws への WebSocket リクエスト
- marimo 編集サーバーへの認証なしのアクセス試行
- marimo によって生成された予期しないターミナルセッション
- marimo プロセスによって起動されたコマンド
- .env、SSH キー、クラウド資格情報、またはノートブックのシークレットへのアクセス
- /terminal/ws 接続直後のアウトバウンドネットワークトラフィック
検査するローカルアーティファクトの例:
docker compose logs vuln
docker compose logs patched
docker compose ps
docker compose exec vuln ps aux
コンテナを停止して削除します:
docker compose down
ボリュームも削除します:
docker compose down -v
必要に応じてローカルの Python 仮想環境を削除します:
rm -rf .venv
このリポジトリは、ローカルでのセキュリティ研究とポートフォリオデモンストレーションのみを目的としています。
許可されていること:
- ローカルホストでのテスト
- Docker のみでの再現
- id、whoami、hostname などの無害な証明コマンド
- 脆弱なバージョンとパッチ適用済みのバージョンの挙動の比較
- 根本原因と検出アイデアの文書化
許可されていないこと:
- 公開されている marimo サーバーに対するテスト
- 所有または管理していないシステムのテスト
- リバースシェル
- 永続化
- 資格情報の窃取
- 破壊的なコマンド
- 横方向の移動
- ボットネットまたはマルウェアの挙動
このプロジェクトは、許可されたローカルテストと防御的なセキュリティ教育のみを目的としています。所有していない、または明示的なテスト許可を得ていないシステムに対して、これらのスクリプトを使用しないでください。
GitHub セキュリティアドバイザリ — GHSA-2679-6mx9-h9xc:
https://github.com/advisories/GHSA-2679-6mx9-h9xc
NVD — CVE-2026-39987:
https://nvd.nist.gov/vuln/detail/CVE-2026-39987
marimo 上流リポジトリ:
https://github.com/marimo-team/marimo
パッチコミット — ターミナル WebSocket への認証検証の追加:
https://github.com/marimo-team/marimo/commit/c24d4806398f30be6b12acd6c60d1d7c68cfd12a
パッチ PR — marimo PR #9098:
https://github.com/marimo-team/marimo/pull/9098
| ファイル | 目的 |
|---|
docker-compose.yml | 脆弱な marimo サービスとパッチ適用済みの marimo サービスを定義 |
vuln/Dockerfile | 脆弱な marimo サービスをビルド |
patched/Dockerfile | パッチ適用済みの marimo サービスをビルド |
vuln/notebook.py | 脆弱なサービスが使用する最小限の marimo ノートブックファイル |
patched/notebook.py | パッチ適用済みのサービスが使用する最小限の marimo ノートブックファイル |
poc/poc.py | 無害なコマンドのみを実行する最小限の害しか及ぼさない証明スクリプト |
poc/rce_poc.py | ローカルラボでターミナルの挙動を観察するためのリアルタイム学習クライアント |
poc/requirements.txt | PoC スクリプト用の Python 依存関係 |
SAFETY.md | 安全規則とスコープの境界 |