MarimoのCVSS 9.3 pre-auth RCEとAstralが公開したuvサプライチェーン防御
目次
2026年4月、Pythonエコシステムのセキュリティで対照的な2つの出来事があった。片方では、人気のPython notebookツールがCVSS 9.3の未認証RCEを抱えたまま公開されており、アドバイザリから10時間以内に実際の侵害が起きた。もう片方では、数百万人の開発者が使うuvとruffを開発するAstralが、2026年に相次いだサプライチェーン攻撃を踏まえた自社の防御体制を公開した。
MarimoのCVSS 9.3 pre-auth RCE(CVE-2026-39987)
オープンソースのPython notebook「Marimo」に、認証なしでシェルを乗っ取れるRCEが発見された。CVE-2026-39987(CVSS v4.0: 9.3)で、Sysdigの調査によればアドバイザリ公開から9時間41分後に実際の悪用が確認されている。
Marimoとは
Marimoは再現性重視のPython notebookで、セルをDAG構造で管理してJupyterのようなグローバル状態の問題を解消する設計になっている。ローカルでの分析・プロトタイピングから、--host 0.0.0.0でLAN公開したチーム共有環境まで幅広く使われている。GitHubスターは1万4000超、PyPI月間ダウンロードは30万件規模のプロジェクトだ。
脆弱性の概要
問題はStarletteベースのバックエンドが持つ /terminal/ws WebSocketエンドポイントにある。
Marimoは edit モードで起動すると、ブラウザ内ターミナルのためにWebSocketサーバーをデフォルトポート2718で開く。他のWebSocketエンドポイント(/ws など)は validate_auth() を呼び出して接続を検証するが、 /terminal/ws は実装漏れにより認証チェックをスキップして websocket.accept() を直接呼び出す。接続を受け入れると pty.fork() 経由でインタラクティブシェルを生成し、接続先に付与する。
# 脆弱なコード(marimo/_server/api/endpoints/terminal.py)
@router.websocket("/terminal/ws")
async def terminal_ws(websocket: WebSocket, ...) -> None:
if not _terminal_supported():
...
# validate_auth() の呼び出しが存在しない
await websocket.accept() # 認証なしで即受け入れ
# pty.fork() でシェルを起動
# 正しい実装例(他のエンドポイント)
@router.websocket("/ws")
async def session_ws(websocket: WebSocket, ...) -> None:
await validate_auth(websocket, app_state) # ここで認証チェック
await websocket.accept()
CWEは CWE-306(Missing Authentication for Critical Function)。認証プロキシを前段に置いていない公開デプロイや --host 0.0.0.0 起動のインスタンスが直接影響を受ける。
攻撃フロー
攻撃に必要なのは対象ホストへのネットワーク到達性だけで、アカウントもトークンも不要だ。
sequenceDiagram
participant A as 攻撃者
participant M as Marimoサーバー
A->>M: ws://target:2718/terminal/ws<br/>認証トークンなし
M-->>A: validate_auth()スキップ<br/>websocket.accept()
M-->>A: pty.fork() → フルシェル
A->>M: id; whoami; env
M-->>A: uid=0(root) / 環境変数一覧
A->>M: cat .env
M-->>A: AWS_ACCESS_KEY / DB_PASSWORD 等
Sysdigのハニーポット観察では攻撃者が以下の順序で動いた。まずマーカー付きコマンドでPoC確認を行い、続いてディレクトリ探索とプロセス確認、最後に .env ファイルからAWSアクセスキー・DBパスワード・アプリシークレットを回収した。偵察から窃取完了まで約3分だったとSysdigは報告している。
echo '---POC-START---'
id
echo '---POC-END---'
注目すべき点として、初回悪用時点ではCVE番号がまだ採番されておらず、公開 PoC コードも存在していなかった。攻撃者がアドバイザリの記述だけから直接武器化したことになる。CVEベースの脅威インテリジェンスでは検知できないタイプの悪用だ。
影響バージョンと露出規模
| 項目 | 内容 |
|---|---|
| CVE | CVE-2026-39987 / GHSA-2679-6MX9-H9XC |
| CVSS v4.0 | 9.3(Critical) |
| CVSS v3.1 | 9.5(Critical) |
| 脆弱バージョン | 0.20.4以下(0.23.0未満) |
| 修正バージョン | 0.23.0 |
| インターネット露出 | 約186件、うち約16%が未認証terminal WSを受容 |
影響を受けない構成として、認証プロキシを前段に持つデプロイ・127.0.0.1バインドのローカル専用起動・run/applicationモードでの起動がある。editモードかつ外部公開している場合のみ脆弱になる。
同種の脆弱性パターンとしては、3月に開示されたAIコーディングエージェント OpenCode の CVE-2026-22812(認証なしHTTPサーバー)・CVE-2026-22813(WebSocket経由のRCE)が近い(/articles/opencode-cve-2026-22812-22813-rce)。LangflowのCVE-2026-33017も公開20時間以内に実攻撃が始まっており、devツール系はターミナル・シェルアクセスを前提とした機能を持ち、認証の境界が曖昧になりがちという共通の弱点がある。
対応
修正は 0.23.0 で提供されている。
pip install --upgrade "marimo>=0.23.0"
ネットワーク分離も並行して行う。デフォルトポートの2718への外部アクセスはファイアウォールでブロックし、共有環境が必要な場合はSSHトンネル経由かVPN経由に限定する。インスタンスがすでに公開状態だった場合、.envに含まれるすべての認証情報(AWSキー・DBパスワード・APIトークン等)は侵害済みとみなして即時ローテーションする。
インフラ全体でJupyter・marimo等のnotebookプラットフォームが稼働していないか棚卸しするタイミングでもある。分析環境はローカルPCで動かすものという認識が強く、意図せず 0.0.0.0 で公開されているケースが少なくない。
Astralが公開したuvとruffのサプライチェーン防御
Pythonリンター ruff とパッケージマネージャー uv を開発するAstralが、自社のサプライチェーンセキュリティ対策を公式ブログで公開した。著者はセキュリティエンジニアのWilliam Woodruff。数百万の開発者が使うツールを提供する側として、2026年に相次いだサプライチェーン攻撃から何を学び、どう守っているかを体系的にまとめている。
この記事が出た背景にはTrivyとLiteLLMの事件がある。2026年3月、TeamPCPがTrivyのCI/CDパイプラインを侵害してPyPIトークンを窃取し、LiteLLMのパッケージを汚染した。同グループはtelnyx Python SDKにもWAVステガノグラフィーで難読化したペイロードを埋め込んでいた。同じくGitHub Actions自体のセキュリティロードマップが公開されたのもこの流れだ。
CI/CDパイプラインの防御
Astralが最初に挙げているのが、GitHub Actionsのワークフロートリガー制御だ。
pull_request_target と workflow_run の2つのトリガーをOrganization全体で禁止している。これらは外部からのPRに対してリポジトリのシークレットにアクセスできる状態でコードを実行できるため、「pwn request」と呼ばれる攻撃の温床になる。実際にUltralytics、tj-actions、Nxの侵害はこのトリガーを悪用されたものだった。
pwn requestとは、pull_request_target トリガーを悪用して、攻撃者が提出したPRのコードをリポジトリの権限で実行させる手法だ。通常の pull_request トリガーではフォークからのPRにシークレットは渡されないが、pull_request_target はベースブランチのコンテキストで走るため、シークレットが露出する。攻撃者はここを突いてCIパイプラインからトークンを盗む。
Actionのピン留めも徹底している。ブランチ名やタグではなくコミットハッシュに固定し、zizmor(GitHub Actionsの静的解析ツール)の unpinned-uses と impostor-commit 監査で検証する。タグは可変(mutable)なので、Trivyの事件ではまさにタグの書き換えで悪意あるコミットに差し替えられた。ハッシュピンなら同じ手口は通用しない。
権限管理ではOrganizationレベルでデフォルトをread-onlyに設定し、すべてのワークフローを permissions: {} で開始してから必要な権限だけを追加する方針をとっている。
# Astralの方針: 最小権限で始める
permissions: {}
jobs:
build:
permissions:
contents: read
外部PRへのコメント問題とGitHub App
GitHub Actionsでは安全に処理できない操作がある。外部PRへのコメント投稿がその代表例だ。
pull_request トリガーではシークレットにアクセスできないためコメントできず、pull_request_target を使えばセキュリティリスクが生じる。Astralはこのジレンマを、GitHub App(astral-sh-bot)を使って解決している。GitHub Appはワークフローの外で動作するため、コードとデータの混在(CIパイプライン内でのシークレット露出)を回避できる。
ただしGitHub Appも万能ではない。SQLインジェクションやプロンプトインジェクションに脆弱な実装をすればアプリ自体が攻撃ベクトルになる。AstralはフレームワークとしてGidgethubを推奨している。
Trusted Publishingでlong-livedクレデンシャルを排除
リリースセキュリティの核がTrusted Publishingだ。PyPI、crates.io、npmの各レジストリで採用している。
Trusted Publishingは、OIDCを使ってCIプロバイダー(GitHub Actionsなど)とパッケージレジストリの間で短命のトークンを交換する仕組みだ。従来のAPIトークンやパスワードのようなlong-lived credential(長期間有効な認証情報)を完全に排除する。
sequenceDiagram
participant GH as GitHub Actions
participant OIDC as GitHub OIDC Provider
participant PyPI as PyPI
participant Fulcio as Sigstore Fulcio
GH->>OIDC: OIDCトークン要求
OIDC-->>GH: 短命トークン発行<br/>(リポジトリ・ワークフロー情報含む)
GH->>PyPI: OIDCトークンで認証<br/>パッケージアップロード
PyPI-->>GH: アップロード成功
GH->>Fulcio: OIDCトークンで署名証明書要求
Fulcio-->>GH: 短命X.509証明書発行
GH->>GH: 一時鍵ペアでアーティファクト署名
GH->>PyPI: Sigstore attestation添付
OIDCトークンにはリポジトリ名、ワークフロー名、ブランチなどの情報が埋め込まれる。PyPI側は「このリポジトリのこのワークフローからのアップロードだけ受け付ける」という設定を持っているので、トークンが盗まれても別のワークフローからは使えない。
Trivy事件ではPyPIのAPIトークンが盗まれてLiteLLMの汚染につながった。Trusted Publishingならそもそもトークンが存在しないので、この攻撃経路自体が消える。
Sigstore attestationで来歴を暗号検証
Trusted Publishingの上に、Sigstoreベースのattestationを乗せている。attestation(アテステーション)は「このバイナリがこのワークフローで、このコミットから作られた」という来歴情報を暗号的に証明する仕組みだ。
技術的にはPEP 740として標準化されている。Sigstoreの構成要素は3つある。
| コンポーネント | 役割 |
|---|---|
| Fulcio | OIDCトークンを受け取り、短命のX.509署名証明書を発行するCA |
| Rekor | 署名ログを記録する透明性ログ。署名が実際に行われたことの証拠を不変に保存する |
| Cosign | コンテナイメージやバイナリに署名・検証するCLIツール |
署名に使う秘密鍵は一時的に生成され、署名後は即座に破棄される(keyless signing)。鍵管理の負担がゼロになるのが大きい。uvのattestationはGitHubのattestationsページで確認できる。
リリースパイプラインの多層防御
Astralのリリースプロセスは複数の防御層で構成されている。
まずGitHubのimmutable releases機能を有効化している。Trivy事件ではタグの強制プッシュで既存リリースのバイナリが差し替えられた。immutable releasesはリリース作成後の変更を禁止するので、同じ手口を防げる。
リリース時のキャッシュも無効化している。GitHub Actionsのキャッシュポイズニング(キャッシュに悪意あるファイルを注入し、後続のビルドで使わせる攻撃)を防ぐためだ。
承認フローも厳格で、デプロイメント環境を分離し、リリースシークレットへのアクセスには別の特権メンバーの承認を必要とする。uvのように多数のリリースジョブがあるリポジトリでは、ost-environment-gateというデプロイメント保護ルールを使って「release-gate」環境から「release」環境へのゲートを設けている。1つのアカウントが侵害されてもリリースできない仕組みだ。
タグ保護ルールセットも重要だ。デプロイメントが成功するまでリリースタグの作成をブロックし、作成後はイミュータブルにする。リリースはmainブランチからのみ許可される。
スタンドアロンインストーラーには、インストーラーのソースコード内にチェックサムを埋め込んでいる。ベンダリングするユーザーがバイナリの完全性を検証できるようにするためだ。
依存関係の管理とクールダウン
Astralは依存関係の更新にDependabotとRenovateを併用し、さらに「クールダウン」を設定している。クールダウンとは、依存パッケージの新バージョンがリリースされてから一定期間待ってからアップデートする仕組みだ。
サプライチェーン攻撃の多くは、悪意あるバージョンの公開直後に最も多くの被害を出す。偽Strapiプラグイン36個の事例がまさにそうだった。クールダウンを入れることで、悪意あるバージョンが検出・削除されるまでの時間を稼げる。uv自体にもクールダウン機能が組み込まれている。
依存関係の追加自体も保守的な方針で、不要な機能は無効化し、バイナリブロブを持ち込む依存は避け、圧縮スキーム関連の依存を削減する計画がある。加えて、上流プロジェクトとの関係構築にも力を入れており、OSS Fundを通じた資金的な支援も行っている。
組織レベルのアクセス制御
管理者アカウントの配布を最小限にし、2FAはTOTP以上を必須化。将来的にはフィッシング耐性のあるWebAuthnやPasskeyへの移行を目指している。
ブランチ保護ルールはOrganization全体で適用され、mainへの強制プッシュを禁止。advisory-* や internal-* といったパターンのブランチ作成もブロックしている。これは攻撃者が「セキュリティアドバイザリ」を装ったブランチを作る手口への対策だ。
AstralはこれらのルールセットをGitHub Gistとして公開しており、他のOSSプロジェクトが参考にできるようにしている。
Trivy事件を振り返ると
Astralの対策をTrivyのCI/CD侵害と突き合わせると、TeamPCPが使った攻撃手法のほぼすべてが対策済みだとわかる。
| TeamPCPの攻撃手法 | Astralの対応策 |
|---|---|
pull_request_targetの悪用でPATを窃取 | Organization全体で当該トリガーを禁止 |
| タグの強制プッシュでActionを差し替え | コミットハッシュへのピン留め + immutable releases |
| 窃取したPyPIトークンでパッケージ汚染 | Trusted Publishingでトークン自体が存在しない |
| 正規バイナリを悪意あるものに差し替え | Sigstore attestationで来歴を暗号検証 |
| 単独アカウント侵害でリリース実行 | 複数メンバーの承認が必要なデプロイメント環境 |
Marimoのpre-auth RCEはdevツールの「使う側」の問題で、Astralのサプライチェーン対策はツールの「作る側」の問題だが、どちらも「Python開発環境を攻撃者に乗っ取られる」という同じ結末につながる。devツールが認証を誤って外部公開し、パッケージが汚染されてCIに流れ込む。攻撃者からすれば開発環境は侵害コストが低く、本番環境への足場にもなりやすい。