MCPのSSRF対策:狙われる3つの経路、2026年のCVE3件、Egress許可リストでの封じ方【MCPセキュリティ】
MCPのSSRF対策を解説。OAuthディスカバリ・URLを受け取るツール・CIMD取得という3つの経路、2026年のCVE3件、仕様のブロック一覧から漏れる範囲、DNS固定まで行う動作確認済みPythonコード、observeからenforceへ進めるEgress許可リストの導入手順まで。
最終更新:2026年10月18日 / 対象:MCP仕様 2026-07-28版(Security Best Practices:Server-Side Request Forgery)、RFC 9728 §7.7、OWASP SSRF Prevention Cheat Sheet
結論(30秒で読める要約)
MCPにおけるSSRF(Server-Side Request Forgery)とは、MCPクライアント・MCPサーバー・認可サーバーが、運用者の意図しない宛先へHTTPリクエストを送らされる攻撃です。狙われるのは 169.254.169.254(クラウドのメタデータ)、localhost 上のサービス、社内APIなど。MCP仕様が名指しで扱っている攻撃の一つで、2026年上半期だけでもMCPサーバー3製品にHigh評価のSSRF脆弱性(CVE)が出ています。
MCPセキュリティとしての対策は、次の3層にまとまります。
- 外向きURLはコードで検証し、接続先を固定する。HTTPSに限定し、ホスト名を名前解決して、解決結果に1つでも非公開アドレスがあれば拒否します。接続は検証したIPに対して行い、リダイレクトは自動で追いません。6章に動作確認済みのPythonモジュールを載せています。
- Egress許可リストをネットワーク側で強制する。コード側のチェックは、ライブラリ内部の通信やリトライ、バグで漏れます。サーバーごとの許可リストをプロセスの外で強制すれば、チェック漏れは「遮断されて記録される接続」に変わります。
- 狙われる先の価値を下げる。メタデータへのアクセスにトークンを必須にし(AWSならIMDSv2)、サーバーのクラウド上の権限を最小にします。リクエストヘッダーで接続先URLを選ばせる設計は避けます。
なお、仕様に載っている非公開アドレスの一覧は出発点としては十分ですが、漏れがあります。5章で補います。
目次
- MCPのSSRFとは
- MCP構成でSSRFが起きる3つの経路
- 2026-07-28版の仕様が求めていること
- 2026年のMCP SSRF脆弱性3件と教訓
- ブロックすべきアドレスと、仕様の一覧の漏れ
- 第1層:コードでURLを検証して固定する(Python・動作確認済み)
- 第2層:プロセスの外でEgress許可リストを強制する
- 第3層:メタデータとクラウド権限の価値を下げる
- URLを受け取るMCPサーバーの設計ルール
- 検知:アラートを設定すべきログ
- MCP SSRF対策チェックリスト(16項目)
- よくある失敗(症状→原因→対処)
- NodeFlareならこう解決できる
- よくある質問
1. MCPのSSRFとは
MCPのSSRFとは、MCPの構成要素がアクセスするURLを攻撃者が全部または一部操作し、その構成要素にしか届かない場所へリクエストを送らせる攻撃です。MCP仕様は「攻撃者がMCPクライアントに意図しない宛先へのHTTPリクエストを送らせ、内部ネットワーク、クラウドのメタデータエンドポイント、その他の保護されたサービスにアクセスし得る攻撃」と定義しています。
MCPで特に問題になる理由は2つあります。
- MCPの構成要素は、自分で選んでいないURLを取りに行く。OAuthのディスカバリでは、クライアントはサーバーから渡されたURLをたどります。また、Webページの取得やAPI呼び出し、ドキュメントの読み込みをモデルの代わりに行うこと自体が役割のMCPサーバーも多くあります。
- 呼び出し元がモデルであることが多い。プロンプトインジェクションを受けたエージェントは、「fetch」や「URLから取り込む」系のツールに、公開URLと同じ気軽さで社内のURLを渡せます。MCP公式のリファレンス実装
fetchサーバーのREADMEにも「このサーバーはローカル/内部のIPアドレスにアクセスでき、セキュリティ上のリスクとなり得る」と注意書きがあります。
仕様が挙げるリスクは5つです。メタデータからの認証情報の流出、内部ネットワークの偵察、POSTによる内部サービスの状態変更、ファイアウォールの迂回、エラーメッセージやOAuthフローを通じたデータの持ち出しです。
2. MCP構成でSSRFが起きる3つの経路
MCPの構成には、信頼できない相手がURLを差し込める場所が3つあります。直すべき担当者がそれぞれ違うので、分けて考えます。
| 経路 | リクエストを送る側 | URLの出どころ | 対策する人 | 仕様の該当箇所 |
|---|---|---|---|---|
| OAuthディスカバリ | MCPクライアント(特にサーバー上で動くもの) | WWW-Authenticate の resource_metadata、Protected Resource Metadataの authorization_servers、認可サーバーメタデータ内の各エンドポイント |
クライアント開発者、サーバー側エージェントの運用者 | Security Best Practices:SSRF |
| URLを受け取るツールやヘッダー | MCPサーバー | ツール引数(url、endpoint、webhook)や、接続先を選ぶリクエストヘッダー |
MCPサーバーの開発者・運用者 | MCP固有の規定なし。OWASP SSRF Cheat Sheetが該当 |
| Client ID Metadata Documents(CIMD) | 認可サーバー | 新しいクライアントが提示する client_id のURL |
認可サーバーの運用者 | Security Best Practices:SSRF Against Authorization Servers |
公開されている解説の多くは1行目に集中しています。仕様が詳しく書いているのがそこだからです。ところが4章で紹介する2026年のCVEは、3件とも2行目、つまり「リクエストの内容でサーバーの接続先URLが決まってしまう」MCPサーバーで起きています。
CI、定期ジョブ、ホスティング型のチャット製品など、サーバー上でエージェントを動かしている場合は1行目も自分ごとです。ノートPC上のクライアントが届くのはそのPCのネットワークまでですが、VPC内のクライアントはメタデータサービスに届きます。
3. 2026-07-28版の仕様が求めていること
仕様のSSRFの節はほとんどがSHOULDで、MUSTは「サーバー上で動くクライアント」向けの1つだけです。関係する規定をまとめます。
| 規定 | 仕様の記述(2026-07-28版) | 対象 |
|---|---|---|
| SSRFを考慮する | サーバーにデプロイされたMCPクライアントは、OAuth関連URLの取得時にSSRFのリスクを考慮し、適切な対策を実装しなければならない(MUST) | サーバー上のクライアント |
| HTTPSに限定 | 本番ではOAuth関連のURLにHTTPSを求めるべき(SHOULD)。開発時のループバック以外の http:// は拒否 |
クライアント |
| 非公開アドレスをブロック | RFC 9728 §7.7の推奨どおり、プライベート・予約済みのIP範囲へのリクエストをブロックすべき(SHOULD) | クライアント |
| IP判定を自作しない | 「IP検証を手作業で実装しないこと。攻撃者は8進数・16進数・IPv4射影IPv6などの表記を悪用し、自作のパーサーはそれを見落としがち」 | 全員 |
| リダイレクト先も検証 | リダイレクト先にも同じ検証を適用すべき(SHOULD)。自動追従の無効化も検討 | クライアント |
| Egressプロキシ | サーバー側クライアントの運用者は、ネットワークポリシーを強制するEgressプロキシ(StripeのSmokescreenなど)の利用を検討すべき(SHOULD) | 運用者 |
| DNSのTOCTOU | 検証時と使用時の間で名前解決の結果を固定することを検討 | 全員 |
| 認可サーバー | 上記の対策は、クライアントメタデータ文書を取得する認可サーバーにも「同様に当てはまる」 | 認可サーバー |
| 危険なURLスキーム | 認可URLには http:// と https:// だけを許可し(MUST)、javascript:、data:、file:、vbscript: を拒否しなければならない(MUST) |
クライアント(OAuth Authorization URL Validation) |
逆に、仕様が書いていないことも押さえておきます。ツールの中でURLを取得するMCPサーバー向けの規定はなく、Egressの既定ポリシーも定義されていません。ここは設計者が決める部分です。
4. 2026年のMCP SSRF脆弱性3件と教訓
3件ともCWE-918(Server-Side Request Forgery)で、深刻度はHighです。内容はアドバイザリに書かれている範囲にとどめます。
| CVE | 製品 | 影響を受ける版/修正版 | CVSS 3.1 | 原因(アドバイザリより) | 教訓 |
|---|---|---|---|---|---|
| CVE-2026-26118 | Microsoft Azure MCP Server | 2.0.0-beta.17より前/2.0.0-beta.17(2026年3月10日) | 8.8 | 低権限で認証済みの攻撃者が、サーバーに意図しない宛先へリクエストを送らせることができた。研究者はマネージドIDのトークンが漏れ得ると指摘 | 狙われるのはサーバーのクラウド上のID。権限を絞り、Egressを制限する |
| CVE-2026-27826 | mcp-atlassian(pip) | 0.17.0未満/0.17.0(アドバイザリ公開2026年2月24日) | 8.2 | streamable-http または sse で動かした場合、X-Atlassian-Jira-Url/X-Atlassian-Confluence-Url ヘッダーを検証せず信頼しており、未認証の呼び出し元が接続先URLを選べた |
接続先ホストをリクエストヘッダーで選ばせない |
| CVE-2026-39974 | n8n-mcp(npm) | 2.47.3以前/2.47.4 | 8.5 | マルチテナントのHTTPモードで、有効な AUTH_TOKEN を持つ呼び出し元がヘッダー経由でインスタンスURLを指定でき、レスポンス本文がJSON-RPCで返っていた |
「認証済み」は「信頼できる」ではない。共有トークンは攻撃者になり得る人を増やす |
3件に共通する点は次のとおりです。
- URLが設定ではなくリクエストから来ていた。3件のうち2件は、リクエストごとに「どのインスタンスにつなぐか」を選べる機能でした。
- ネットワーク側の対策は、パッチ適用後も残す。n8n-mcpのアドバイザリは、プライベート・リンクローカル宛ての外向き通信をネットワーク層でブロックし、アップグレード後もそれを続けるよう勧めています。mcp-atlassianのアドバイザリは、HTTPエンドポイントへのアクセス制限と、インスタンスメタデータの設定・IAM権限の見直しを挙げています。
- SSRFはプロンプトインジェクションの入口にもなる。mcp-atlassianのアドバイザリは、攻撃者が用意した内容がツールの結果に混ざることも影響として挙げています。攻撃者が選んだURLを取りに行くサーバーは、攻撃者が選んだ文章をモデルに返すサーバーでもあります。この側面はMCPのTool Poisoning対策ガイドで扱っています。
5. ブロックすべきアドレスと、仕様の一覧の漏れ
仕様はRFC 9728 §7.7にならい、次の範囲を挙げています。これが最低ラインです。
| 範囲 | 内容 | 仕様の一覧にあるか |
|---|---|---|
10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 |
プライベートIPv4(RFC 1918) | あり |
127.0.0.0/8、::1 |
ループバック | あり |
169.254.0.0/16 |
リンクローカル。AWS・GCP・Azureのメタデータ 169.254.169.254 を含む |
あり |
fc00::/7 |
ユニークローカルIPv6。AWSのIPv6メタデータ fd00:ec2::254 もここに入る |
あり |
fe80::/10 |
リンクローカルIPv6 | あり |
100.64.0.0/10 |
共有アドレス空間(RFC 6598)。Alibaba Cloudのメタデータは 100.100.100.200 |
なし |
0.0.0.0/8 |
「このネットワーク」。0.0.0.0 はローカルの待ち受けに届くことが多い |
なし |
::ffff:0:0/96 |
IPv4射影IPv6。上のどのアドレスも包んで表現できる | 表記のトリックとして触れているだけ |
| マルチキャスト、ブロードキャスト、ドキュメント用などの予約済み範囲 | 宛先として使う理由がない | 「予約済み」に含まれると読める |
実務上のルールは、独自のリストを管理するのではなく、標準ライブラリが「グローバルに到達可能」と判定したアドレスだけを許可することです。Pythonなら、IPv4射影IPv6を元のIPv4に戻したうえで ipaddress.ip_address(x).is_global を使えば、100.64.0.0/10 や 0.0.0.0 も含め、表のすべての範囲が False になります。仕様の「IP検証を自作しない」もこの意味です。
6章のテストで確かめた、見落とされがちな事実が2つあります。
- 数値や16進・8進の表記でもループバックになる。
2130706433、0x7f.0.0.1、0177.0.0.1というホスト名は、OSのリゾルバーでいずれも127.0.0.1になります。URLの文字列を正規表現で調べても防げませんが、解決後のアドレスを調べれば防げます。 - DNSの応答は最初の1つではなく、すべて調べる。公開アドレスと非公開アドレスを両方返す名前は拒否します。そうしないと、接続が非公開側に着地することがあります。
6. 第1層:コードでURLを検証して固定する(Python・動作確認済み)
次のモジュールは、仕様とOWASP SSRF Prevention Cheat Sheetが説明している内容を、標準ライブラリだけで約70行にしたものです。HTTPSに限定し、URL内の認証情報を拒否し、必要ならホスト名の許可リストを適用します。名前解決の結果に1つでも非公開アドレスがあれば拒否し、検証したIPにそのまま接続します。SNIと証明書の検証は元のホスト名で行い、リダイレクトは追いません。Python 3.13で動作を確認しています。
"""ssrf_guard.py - validate and pin outbound URLs for MCP clients and servers."""
import http.client
import ipaddress
import socket
import ssl
from urllib.parse import urlsplit
class BlockedURL(ValueError):
pass
def _is_public(ip) -> bool:
# Unwrap IPv4-mapped IPv6 (::ffff:a.b.c.d) before checking.
if isinstance(ip, ipaddress.IPv6Address) and ip.ipv4_mapped:
ip = ip.ipv4_mapped
# is_global is False for RFC 1918, loopback, link-local (169.254/16),
# shared 100.64/10, fc00::/7, fe80::/10 and other reserved ranges.
return ip.is_global and not ip.is_multicast
def resolve_and_check(url: str, allowed_hosts: set[str] | None = None):
"""Return (host, port, pinned_ip) or raise BlockedURL."""
parts = urlsplit(url)
if parts.scheme != "https":
raise BlockedURL(f"scheme not allowed: {parts.scheme!r}")
if parts.username or parts.password:
raise BlockedURL("userinfo in URL")
host = (parts.hostname or "").rstrip(".").lower()
if not host:
raise BlockedURL("missing host")
if allowed_hosts is not None and host not in allowed_hosts:
raise BlockedURL(f"host not on allowlist: {host}")
port = parts.port or 443
try:
infos = socket.getaddrinfo(host, port, type=socket.SOCK_STREAM)
except socket.gaierror as e:
raise BlockedURL(f"DNS failure: {host}") from e
ips = {ipaddress.ip_address(info[4][0].split("%")[0]) for info in infos}
bad = [str(ip) for ip in ips if not _is_public(ip)]
if bad:
# Reject if ANY answer is internal: attackers mix public and private records.
raise BlockedURL(f"{host} resolves to non-public address {bad}")
return host, port, str(sorted(ips, key=str)[0])
class _PinnedHTTPS(http.client.HTTPSConnection):
"""Connect to the validated IP, but keep SNI and certificate checks on the hostname."""
def __init__(self, host, pinned_ip, port, timeout):
super().__init__(host, port, timeout=timeout, context=ssl.create_default_context())
self._pinned_ip = pinned_ip
def connect(self):
sock = socket.create_connection((self._pinned_ip, self.port), self.timeout)
self.sock = self._context.wrap_socket(sock, server_hostname=self.host)
def safe_get(url: str, allowed_hosts: set[str] | None = None,
timeout: float = 5.0, max_bytes: int = 1_000_000) -> bytes:
"""GET with no automatic redirects, a pinned IP and a size cap."""
host, port, ip = resolve_and_check(url, allowed_hosts)
parts = urlsplit(url)
path = (parts.path or "/") + (f"?{parts.query}" if parts.query else "")
conn = _PinnedHTTPS(host, ip, port, timeout)
try:
conn.request("GET", path, headers={"Host": host, "Accept": "application/json"})
resp = conn.getresponse()
if 300 <= resp.status < 400:
# Do not follow. Re-validate the Location with resolve_and_check if you must.
raise BlockedURL(f"redirect not followed: {resp.status}")
body = resp.read(max_bytes + 1)
if len(body) > max_bytes:
raise BlockedURL("response too large")
return body
finally:
conn.close()
resolve_and_check に17パターンの入力を与えた結果です。
| 入力 | 結果 | 理由 |
|---|---|---|
https://169.254.169.254/latest/meta-data/ |
拒否 | リンクローカル |
http://example.com/ |
拒否 | HTTPSではない |
https://127.0.0.1/、https://localhost/ |
拒否 | ループバック |
https://2130706433/、https://0x7f.0.0.1/、https://0177.0.0.1/ |
拒否 | すべて 127.0.0.1 に解決される |
https://[::ffff:169.254.169.254]/ |
拒否 | IPv4射影のリンクローカル |
https://[fd00:ec2::254]/ |
拒否 | ユニークローカルIPv6(AWSのIPv6メタデータ) |
https://100.100.100.200/ |
拒否 | 共有アドレス空間(Alibabaのメタデータ) |
https://10.0.0.5/ |
拒否 | RFC 1918 |
https://0.0.0.0/ |
拒否 | 「このネットワーク」 |
https://user:pw@example.com/ |
拒否 | URLに認証情報 |
| 公開と非公開のアドレスを1つずつ返す名前(DNSをモック) | 拒否 | 非公開の応答が1つでもあれば失敗 |
https://8.8.8.8/、https://[2606:4700:4700::1111]/ |
許可 | 公開アドレス |
allowed_hosts={"api.example.com"} を指定して https://8.8.8.8/ |
拒否 | 許可リストにない |
使う前に知っておくべき限界です。
- 検証部分はテストしたが、実際の取得はテストしていない。テスト環境から直接の名前解決ができなかったため、
safe_getを実在のホストに対しては動かしていません。自社のエンドポイントで先に確認してください。 - 外向きの呼び出しはすべて、この1つの関数を通す。守れるのはこの関数を呼ぶコードだけです。独自にHTTPリクエストを送るライブラリ(SDK、PDFレンダラー、ヘッドレスブラウザなど)は素通りします。第2層が必要な理由はここにあります。
- HTTPプロキシ経由なら前提が変わる。プロキシを通す場合、名前解決と接続を行うのはプロキシです。ポリシーはプロキシ側に置きます(たとえばSmokescreenは名前を解決し、既定で非公開アドレスへの接続を拒否します)。
- できるだけ許可リストを使う。OWASPのチートシートは拒否リストを「回避されやすい」とし、宛先が事前に分かるなら許可リスト(このコードでは
allowed_hosts)を使うよう勧めています。
7. 第2層:プロセスの外でEgress許可リストを強制する
Egress許可リストとは、サーバーが接続してよい宛先を限定し、それをサーバー自身のコードではなくネットワークやホスト側で強制する仕組みです。仕様の「MCPクライアントの外向きアクセスを制限するネットワークポリシーを構成する」に当たり、n8n-mcpのアドバイザリがアップグレード後も続けるよう勧めている対策でもあります。
層ごとに防げるものは次のとおりです。
| 失敗のパターン | コードでの検証(第1層) | Egress許可リスト(第2層) |
|---|---|---|
ツール引数が 169.254.169.254 を指している |
ツールが検証関数を使っていれば防げる | リストになければ防げる |
| サードパーティのライブラリが独自に通信する | 防げない | 防げる |
| 設定し忘れたHTTPクライアントがリダイレクトを追う | 防げない | 転送先がリストになければ防げる |
| 検証と接続の間のDNSリバインディング | 防げる(IPを固定) | 強制方式による |
| 攻撃者の公開ホストへのデータ持ち出し | 防げない(公開アドレスなので) | 防げる |
| リストにある正規のAPIの悪用 | 防げない | 防げない(スコープとログで対処) |
最後の2行が、両方必要な理由です。公開アドレスかどうかのチェックは内部への攻撃を止めますが、attacker.example へのデータ送信は止めません。許可リストは両方を止めますが、それはリストが狭い場合に限られます。
本番を壊さずに導入する手順です。
- 棚卸し。各サーバーを記録のみのモード(observe)で1〜2週間動かし、名前解決や接続をした宛先をすべて記録します。
- 見直し。通常は短いリストになるはずです。ラップしているSaaSのAPI、IdP、ビルド時のパッケージレジストリくらい。それ以外の宛先には、担当者と理由を確認します。
- フリート全体ではなくサーバーごとに強制する。共有の許可リストは「どれかのサーバーが必要とするもの全部」に膨らみ、全サーバーが全員分の宛先を持つことになります。
- ワイルドカードより完全なホスト名を使う。
*.amazonaws.comを許可すると、自社のバケットだけでなく、そのプラットフォーム上の全顧客のバケットが宛先になります。 - 許可リストの変更はコード変更と同じに扱う。レビューを通し、リスト外の宛先への通信が起きたらアラートを出します。
8. 第3層:メタデータとクラウド権限の価値を下げる
それでもリクエストが通ってしまった場合、被害の大きさは相手が何を返すかで決まります。クラウド側の次の設定で小さくできます。
| 設定 | 効果 | 出典 |
|---|---|---|
AWS:IMDSv2を必須にする(HttpTokens = required) |
メタデータの取得に、/latest/api/token へのPUTで得るセッショントークンが必要になる。GETだけの単純なSSRFでは取得できない |
AWS EC2ユーザーガイド。OWASPのチートシートもIMDSv2を「追加の多層防御」と位置づける |
| AWS:PUT応答のホップ数上限を低くする | トークン応答が届く範囲を制限する。ただしAWSは、コンテナ環境ではホップ数 1 が問題を起こし得るとしているので、下げる前に検証する |
AWS EC2ユーザーガイド |
| Alibaba Cloud:セキュリティ強化モード | 100.100.100.200 のメタデータに一時トークンが必要になり、通常のリクエストは403になる |
Alibaba Cloud ECSドキュメント |
| ワークロードのIDを最小権限にする | マネージドIDやIAMロールのトークンが漏れても、届くのはサーバーが必要とする範囲だけになる | CVE-2026-26118のアドバイザリが示す対策 |
これらは第1層・第2層の代わりにはなりません。守れるのはメタデータサービスだけで、社内API、データベース、管理画面は守れません。
9. URLを受け取るMCPサーバーの設計ルール
社内SaaSをMCPで公開する場合、一番安全なURLは呼び出し元に指定させないURLです。次のルールは、3件のCVEと、OWASPチートシートの「リクエストは自分で組み立てる」という助言から導いたものです。
| ルール | やめること | やること |
|---|---|---|
| 接続先は受け取らず、設定で決める | どのインスタンスにつなぐかをヘッダーや引数で指定させる | ベースURLはサーバーの設定に置き、テナントとURLの対応はサーバー側で持つ |
| URLではなくIDを受け取る | fetch_issue(url) |
fetch_issue(issue_key) とし、許可済みのベースURLから組み立てる |
| URLを受け取るなら範囲を絞る | 任意のURLを受け付ける | 検証関数の allowed_hosts と、Egress許可リストを併用する |
| 返す情報を減らす | 上流のレスポンス本文やエラーメッセージをそのまま返す | outputSchema で整形した項目だけを返し、エラーは内部の応答を含まない汎用的なものにする |
| テナントを資格情報で分ける | 複数の運用者で1つの AUTH_TOKEN を共有する |
呼び出し元ごとに資格情報を発行し、使えるツールを限定する |
| stdioはローカルにとどめる | stdioサーバーを認証なしでHTTP公開する | HTTPトランスポートには認証を必須にする。仕様もローカルサーバーはstdioか制限付きのIPCを使うべき(SHOULD)としている |
stdioサーバーをどうしてもリモートで動かしたい場合は、開いたポートではなく、認証付きのHTTPブリッジの後ろに置きます。代表的なブリッジの比較はstdio MCPサーバーをリモートで動かす手順にまとめています。
10. 検知:アラートを設定すべきログ
SSRFの試みは、ネットワークの出口とツール引数に痕跡を残します。次のものはアラートに値します。
| シグナル | 見る場所 | 意味 |
|---|---|---|
| プライベート・リンクローカル・メタデータのアドレスへの接続試行 | Egressログ、ファイアウォール | MCPサーバーからの通信としてはほぼ正当な理由がない |
| 許可リストにない新しい外部宛先 | observe/enforceモードのEgressログ | SSRFや持ち出しの可能性。または未把握の依存関係 |
検証関数が投げた BlockedURL 例外 |
アプリケーションログ | URLを受け取るツールが探られている |
IPアドレスの直書き、localhost、変わった表記を含むツール引数 |
リクエストログ(値そのものではなく、該当したことを記録) | 典型的な探りのパターン |
| サーバーのクラウドIDの想定外の利用 | クラウドの監査ログ(CloudTrail、Azure Activity Logなど) | メタデータへのSSRFが成功したときの典型的な結果 |
リクエストごとに何を記録し、どう改ざんを防ぐかを含め、SSRFを脅威モデル全体の中で位置づけたものがMCPセキュリティのベストプラクティスとチェックリストです。
11. MCP SSRF対策チェックリスト(16項目)
サーバー上で動くクライアント・エージェント
- [ ] OAuthディスカバリのURLはHTTPSに限定している(ループバックの
http://は開発時のみ) - [ ] ディスカバリURLの解決結果をすべて調べ、非公開アドレスが1つでもあれば失敗させている
- [ ] リダイレクトを自動で追わないか、転送のたびに検証し直している
- [ ]
javascript:、data:、file:、vbscript:の認可URLを拒否している - [ ] サーバー側クライアントのディスカバリ通信を、Egressプロキシかネットワークポリシーに通している
MCPサーバー
- [ ] リクエストヘッダーや引数で接続先ホストを選ばせず、ベースURLは設定で決めている
- [ ] できる限り、ツールはURLではなくIDを受け取る
- [ ] 外向きのHTTP呼び出しはすべて、解決・検証・固定を行う1つの関数を通している
- [ ] IP判定は自作の正規表現やリストではなく、標準ライブラリの分類を使っている
- [ ] クライアントに返すレスポンスやエラーに、上流の生の本文を含めていない
- [ ] 呼び出し元ごとに資格情報を発行し、使えるツールを限定している
ネットワークとクラウド
- [ ] サーバーごとにEgress許可リストを持ち、observe期間を経てから導入している
- [ ] サーバーのワークロードから、プライベート・リンクローカル・
100.64.0.0/10・メタデータのアドレスに届かない - [ ] AWSではIMDSv2を必須にしている(他のクラウドでも同等のトークン方式を有効にしている)
- [ ] サーバーのワークロードIDには、ツールに必要な権限しかない
認可サーバー
- [ ] Client ID Metadata Documentsの取得にも、同じ検証関数とEgressポリシーを適用している
12. よくある失敗(症状→原因→対処)
| 症状 | 原因 | 対処 |
|---|---|---|
テストでは内部IPを止められたのに、0x7f.0.0.1 が通る |
解決後のアドレスではなく、URLの文字列を調べている | 先に名前解決し、すべての応答を調べる |
| チェックは通ったのに、接続先が非公開IPだった | DNSリバインディング(接続時に2回目の名前解決が走る) | 検証したIPに接続し、SNIは元のホスト名で行う |
| 自分のコードでは止まるのに、メタデータに届く | 依存ライブラリが独自にHTTP通信をしている | プロセスの外でEgress許可リストを強制する |
| 公開アドレスだけに絞ったのに、データが外に出た | 持ち出し先が攻撃者の公開ホストだった | 非公開を拒否するだけでなく、宛先を許可リストで限定する |
| 許可リストがいつの間にかワイルドカードだらけになった | フリート全体で1つの許可リストを共有している | サーバーごとのリストにし、完全なホスト名で書き、変更をレビューする |
| 有効なトークンを持つテナントからSSRFされた | 共有トークンと、リクエストごとのインスタンスURL指定 | テナントごとに資格情報を分け、URLはサーバー側で対応付ける |
| パッチを当てたのに、アドバイザリがネットワーク側の遮断も勧めている | パッチが直すのは1つのコード経路で、攻撃の類型ではない | アップグレード後もネットワーク層の遮断を残す |
13. NodeFlareならこう解決できる
NodeFlareは、各MCPサーバーを専用の隔離VMで動かし、すべてのリクエストを認証・認可するプロキシの後ろに置きます。外向き通信のルールも設定できます。この記事でいう第2層と、SSRFの被害を抑える仕組みの多くをカバーしますが、第1層(ツールのコード内でのURL検証)と、クライアント側のOAuthディスカバリは利用者側の担当です。記事で挙げたリスクと、ドキュメントに記載された機能の対応は次のとおりです。
| この記事のリスク | NodeFlareの機能 | 内容 |
|---|---|---|
| ツールやライブラリが社内ホストに届く、データを外に持ち出す(7章) | Egress制御 | モードは off(既定)、observe(ブロックせず、サーバーが名前解決した宛先を記録)、enforce(リストにない宛先をブロック)。許可リストは最大100件で、ホスト名、先頭ワイルドカード1つ(*.example.com など)、IPを指定可能。ホスト側のDNSとnftablesで強制 |
| サーバーに必要な宛先が分からない(7章の手順1) | observeモードとEgressログ | observe で動かし、Logs > Egress で宛先を確認してから enforce に切り替える |
| リクエストごとの接続先指定を、トークンを持つ誰もが悪用できる(4章) | ツール単位のスコープ | tools:call:<ツール名>、tools:list、resources:read:<uri> などのスコープを転送前に照合し、一致しなければJSON-RPCエラーを返してサーバーには届かない |
| 盗まれたトークンで未知のネットワークから探られる | ネットワークアクセス制御(IP許可リスト) | ポリシーを restricted にし、許可するIPv4/IPv6のCIDRを指定。認証前・VM起動前に強制し、リストが空なら全拒否(fail closed) |
| URLを受け取るツールに届くため、キーを総当たりされる | 失敗したキーによるロック | 誤ったAPIキーが繰り返されると、そのIPを10分間ロック |
| 侵害されたサーバーが他の認証情報を読む(8章) | Firecracker microVMによる分離と、暗号化されたシークレット | 各サーバーは専用のFirecracker microVMで動作。環境変数は保存時に暗号化され、VMの起動時にのみ注入され、ビルド引数・イメージレイヤー・ログには残らない |
| 探りの痕跡が残らない(10章) | リクエストログ | リクエストごとに outcome(scope_denied、rate_limited など)、http_status、caller_kind、client_ip を記録。保持期間はFreeで7日、Pro/Teamで30日 |
利用者側に残る作業もあります。NodeFlareのEgress強制はDNSクエリのホスト名で判定し、通信内容は復号しません。また、off モードでプライベートアドレスやメタデータのアドレスがブロックされるとはドキュメントに書かれていません。既定値に頼らず、狭いリストで enforce にしてください。ツールのコード内でのURL検証、OAuthディスカバリ時のMCPクライアントの保護は行いませんし、プロンプトインジェクションを受けたモデルがツールにURLの取得を頼むこと自体も防げません。NodeFlareが制限するのは、そのリクエストが届く先です。
最小の始め方:
- MCPサーバーをデプロイし(既定で非公開)、呼び出し元ごとに、必要な
tools:call:<ツール名>スコープだけを持つmcp_*形式のAPIキーを発行します。 - Egress制御を1週間
observeで動かし、Logs > Egress を確認してから、完全なホスト名のリストでenforceに切り替えます。
1サーバーまでは無料で試せます。NodeFlareを始める
14. よくある質問
MCPのSSRFとは何ですか?
MCPクライアント・サーバー・認可サーバーが、クラウドのメタデータ、localhost 上のサービス、社内APIなど、運用者の意図しない宛先へリクエストを送らされる攻撃です。MCP仕様のSecurity Best Practicesで扱われています。主な入口は、OAuthディスカバリのURLと、URLを受け取るツールです。
MCP仕様はSSRF対策を義務づけていますか?
一部だけです。2026-07-28版では、サーバー上で動くMCPクライアントはOAuth関連URLの取得時にSSRFのリスクを考慮しなければならない(MUST)とされ、HTTPSの要求、非公開IP範囲のブロック、リダイレクト先の検証、Egressプロキシの検討がSHOULDです。ツールでURLを取得するMCPサーバー向けの規定はないため、そこはサーバー開発者の判断になります。
MCPクライアントはどのIP範囲をブロックすべきですか?
最低限、仕様の一覧にある 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、::1、169.254.0.0/16、fc00::/7、fe80::/10 です。加えて 100.64.0.0/10(Alibaba Cloudのメタデータは 100.100.100.200)、0.0.0.0/8、IPv4射影IPv6の表記もブロックします。標準ライブラリがグローバルと判定したアドレスだけを許可するのが一番簡単です。
169.254.169.254をブロックすれば十分ですか?
十分ではありません。メタデータサービスは fd00:ec2::254(AWSのIPv6)や 100.100.100.200(Alibaba Cloud)にもあり、SSRFはプライベート範囲にある社内API、データベース、管理画面も狙います。0x7f.0.0.1 のような表記も解決すると遮断対象のアドレスになるので、URLの文字列ではなく解決後のアドレスを調べてください。
MCPサーバーでDNSリバインディングを防ぐには?
ホスト名を1回だけ名前解決し、返ってきたアドレスをすべて検証してから、検証したアドレスに接続します。SNIと証明書の検証は元のホスト名で行います。仕様はこれを「検証時と使用時の間で名前解決の結果を固定する」と表現しています。6章に動作確認済みのPython実装があります。
OAuthディスカバリでMCPクライアントはリダイレクトを追うべきですか?
無条件に追うべきではありません。仕様は、リダイレクト先にも同じURL検証を適用すべき(SHOULD)とし、自動追従を無効にして転送ごとに検証することを勧めています。普通に見えるURLが社内アドレスへ転送するのは、仕様が挙げる攻撃パターンの一つです。
MCPサーバーのEgress許可リストとは何ですか?
MCPサーバーが接続してよい宛先だけを並べたリストで、サーバーのコードではなくネットワークやホスト側で強制するものです。サードパーティのライブラリが送るリクエストや公開ホストへの持ち出しなど、コード側のIPチェックでは防げないものを止められます。まず実際の通信を観察してから、サーバーごとに強制します。
MCPのSSRF対策は許可リストと拒否リストのどちらを使うべきですか?
宛先が決まっているなら許可リストです。1つのSaaS APIをラップするMCPサーバーの多くはこれに当たります。OWASP SSRF Prevention Cheat Sheetは拒否リストを「回避されやすい」としています。任意のサイトに届く必要があるツールに限って公開アドレスのチェックを使い、Egressログと組み合わせます。
SSRF脆弱性が見つかったMCPサーバーはありますか?
2026年には、Microsoft Azure MCP Server(2.0.0-beta.17より前)のCVE-2026-26118、mcp-atlassian(0.17.0未満)のCVE-2026-27826、n8n-mcp(2.47.3以前)のCVE-2026-39974が公開されています。いずれもCWE-918で、CVSSは8.2〜8.8です。アップグレードしたうえで、ネットワーク層の遮断も残してください。
プロンプトインジェクションでMCPツール経由のSSRFは起きますか?
起きます。受け取ったURLをそのまま取得するツールなら、プロンプトインジェクションを受けたエージェントが社内アドレスを渡せます。MCP公式の fetch サーバーのREADMEも「ローカル/内部のIPアドレスにアクセスできる」と注意しています。モデルの判断はセキュリティ境界にならないので、ツール側でのURL検証とEgress制限を行ってください。
IMDSv2でSSRFは防げますか?
AWSのメタデータへの被害は減らせますが、SSRFそのものは防げません。IMDSv2はPUTで取得するセッショントークンを必須にするため、GETだけの単純なSSRFではトークンを得られず、OWASPも多層防御の一つと位置づけています。URLの検証とEgress制限をしない限り、社内APIなど他のサービスには届いてしまいます。
まとめ
MCPのSSRFは、クライアントのOAuthディスカバリ、URLやヘッダーを受け取るMCPサーバーのツール、Client ID Metadata Documentsを取得する認可サーバーの3か所で起きます。2026年のCVEは、いずれもリクエストの内容で接続先URLが決まるサーバーで起きました。対策は3層です。HTTPSに限定し、名前解決し、非公開の応答を1つでも拒否し、接続先を固定するコード側の検証。プロセスの外で強制する、サーバーごとのEgress許可リスト。そしてIMDSv2や最小権限のIDなど、残った標的の価値を下げるクラウド側の設定です。そのうえで、遮断された試みと新しい宛先をログで追い、パッチ適用後もネットワーク側の遮断は残しておきます。
参考資料(一次情報)
- MCP Specification 2026-07-28: Security Best Practices (SSRF)
- RFC 9728: OAuth 2.0 Protected Resource Metadata(§7.7)
- OWASP SSRF Prevention Cheat Sheet
- Microsoft MSRC: CVE-2026-26118
- GitHub Advisory GHSA-7r34-79r5-rcc9(mcp-atlassian、CVE-2026-27826)
- GitHub Advisory GHSA-4ggg-h7ph-26qr(n8n-mcp、CVE-2026-39974)
- MCP reference servers: fetch README
- Stripe Smokescreen
- AWS: Configure the Instance Metadata Service options
- Alibaba Cloud: Instance metadata
- NodeFlare Docs
