Model Context Protocol(MCP)は、LLMに外部ツールを使わせる標準プロトコルとしてAnthropicが公開した仕様である。採用が一気に広がった結果、MCPサーバーそのものが攻撃対象になる事例が2025年後半から急増している。本記事では、自分がレビューで指摘することの多い7つの落とし穴を、防御側の実装例とともに整理する。
MCPとは
LLMクライアント(Claude Desktop、各種IDE、Agentフレームワーク)が、ローカル/リモートのMCPサーバーに接続し、tools/list で能力を確認し、tools/call で実行するだけのシンプルなプロトコルである。stdioまたはHTTP/SSEで通信する。
落とし穴1: 認可なしでファイルシステムを公開
最もありがちな事故。fs_read のようなツールを無制限に公開すると、LLMに「全ファイル読め」と指示されれば丸見えになる。
from pathlib import Path
ALLOW_ROOT = Path("/srv/mcp/sandbox").resolve()
def safe_read(path: str) -> str:
p = (ALLOW_ROOT / path).resolve()
if not p.is_relative_to(ALLOW_ROOT):
raise PermissionError("path traversal")
if p.stat().st_size > 1_000_000:
raise ValueError("file too large")
return p.read_text(encoding="utf-8")
落とし穴2: コマンド実行ツールのホワイトリスト不在
shell ツールを生で提供すると、LLMが任意コマンドを走らせられる。必ずコマンド名と引数のホワイトリストで絞る。
ALLOWED = {"ls", "git", "pytest"}
def run(cmd: list[str]) -> str:
if cmd[0] not in ALLOWED:
raise PermissionError(cmd[0])
import subprocess
r = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
return r.stdout[:8000]
落とし穴3: 環境変数・シークレットの反射
MCPサーバープロセスの環境変数にAPIキーを入れたまま、env ツールで覗かれる事故がある。
def get_env(name: str) -> str:
if name.startswith(("AWS_", "ANTHROPIC_", "OPENAI_", "GITHUB_")):
raise PermissionError("blocked key")
import os
return os.environ.get(name, "")
落とし穴4: ツール結果経由のプロンプトインジェクション
MCPが取得した外部データにインジェクション文字列が含まれ、LLM側が踊る。サーバー側で事前サニタイズできるものはしておく。
def wrap(text: str) -> str:
text = text.replace("</tool_result>", "<!--escaped-->")
return f"<tool_result trust='low'>\n{text}\n</tool_result>"
落とし穴5: ネットワークアクセス無制限
外部HTTPにアクセスできるツールを無制限に公開すると、SSRFやデータ持ち出しに使われる。URLのホスト名をホワイトリストで絞る。
from urllib.parse import urlparse
ALLOW_HOSTS = {"api.example.com", "docs.example.com"}
def safe_fetch(url: str) -> str:
host = urlparse(url).hostname or ""
if host not in ALLOW_HOSTS:
raise PermissionError(host)
import urllib.request
with urllib.request.urlopen(url, timeout=5) as r:
return r.read(1_000_000).decode("utf-8", errors="replace")
落とし穴6: 認証なしのHTTPエンドポイント
リモートMCPをHTTP/SSEで公開する場合、誰でも叩けば tools/call できてしまう。最低でもトークン認証、理想はmTLS。
def require_token(request):
auth = request.headers.get("Authorization", "")
if auth != f"Bearer {EXPECTED_TOKEN}":
raise PermissionError()
落とし穴7: 監査ログ不在
誰のLLMセッションが、いつ、どのツールを、どの引数で叩いたかを必ず残す。
import json, time, hashlib
def audit(session_id: str, tool: str, args: dict):
rec = {
"ts": time.time(),
"session": session_id,
"tool": tool,
"args_hash": hashlib.sha256(json.dumps(args,sort_keys=True).encode()).hexdigest(),
}
with open("/var/log/mcp/audit.jsonl", "a") as f:
f.write(json.dumps(rec) + "\n")
まとめ
MCPサーバーは「LLMに与えるOSのようなもの」と捉えると設計方針が定まる。最小権限、ホワイトリスト、サニタイズ、監査の4原則を全ツールに適用するだけで、サプライチェーン経由の致命的事故はかなり防げる。自前でMCPを書く前に、既存の公式SDKが提供するセキュリティ機能を熟読することを強く勧める。