0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【2026年版】MCPサーバーのセキュリティリスク ─ 外部ツール連携7つの落とし穴

0
Posted at

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が提供するセキュリティ機能を熟読することを強く勧める。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?