Grafana 公式の MCP サーバー、mcp-grafana。Docker Hub で190万回以上ダウンロードされているこのサーバーに、2026年8月、こんな弱点が報告されました。
自分で適当に作ったセッションIDを付けるだけで、ログインなしにツールを呼べた。
呼ばれたツールは、サーバーに設定された Grafana のサービスアカウントの権限で動きます。そのアカウントに許された操作は、攻撃者もそのまま実行できる状態でした。
この穴は 8月10日の v1.1.0 で修正済みです。ただし後で書くとおり、上げただけでは塞がりません。
ちょうど同じ時期、MCP の仕様は 2026-07-28 版でセッションIDそのものを廃止しました。では、最新の仕様に上げていれば、この穴は塞がっていたのでしょうか。
私の答えは「塞がらない」です。むしろ新しい仕様には、同じ種類の失敗をしやすい場所が一つ増えています。その場所で起きる攻撃を手元で4つ再現し、十数行のコードで防ぐところまで試しました。
番号札の形しか見ていなかった
報告したのは Pillar Security です。経緯はこうです。
| 日付 | 出来事 |
|---|---|
| 2026-08-02 | Grafana に報告 |
| 2026-08-10 | 修正版 v1.1.0 リリース |
| 2026-08-11 | CVE-2026-19516 公開(CVSS 9.1) |
報告には2つの弱点が含まれていました。CVE 番号が付いているのは、X-Grafana-URL ヘッダーで外向きリクエストの送り先を変えられた SSRF のほうです。もう一つが、今回の主役のセッション偽造です。Grafana 自身、このサーバーは「受け付ける側の認証なしで出荷されていた」と説明しています。
サーバーは、届いたセッションIDが正しい形式かどうかだけを確かめていました。本当に自分が発行したIDかどうかは見ていません。
銀行の窓口でたとえると、番号札の形と色だけ見て、窓口に通していた状態です。番号札は自分で紙を切って作れます。
v1.1.0 では Bearer トークン認証が入り、X-Grafana-URL ヘッダーの対応は削除されました。ただしトークン認証は任意です。--server-auth-token(または環境変数 MCP_GRAFANA_SERVER_TOKEN)を設定したときだけ有効になります。設定しないまま外部から届く場所に置くと、起動時にセキュリティエラーをログに出すものの、サーバー自体は起動します。
v1.1.0 に上げたから大丈夫、とはなりません。設定まで確認が必要です。
最新の MCP 仕様なら守れたのか
MCP の仕様は 2026-07-28 版で大きく変わりました。最初の initialize も Mcp-Session-Id もなくなりました。1回のリクエストだけで処理が完結する、普通の Web API に近い形です(変更の理由は kenimoto.dev の解説記事 にまとめました)。
セッションIDがなくなれば、偽造もできなくなりそうに見えます。当てはめてみると、こうなります。
| 起きたこと | 2026-07-28 版なら | 理由 |
|---|---|---|
| セッションIDの偽造 | 守れない | IDはなくなるが、認証がなければIDすら要らずにツールを直接呼べる |
| SSRF(送り先の差し替え) | 守れない | ツールの作り方の問題で、通信の仕様とは関係がない |
(新しく増える場所)requestState
|
同じ失敗ができる | サーバーが客に預けた状態を、確かめずに信じると同じ穴になる |
そもそも、旧仕様(2025-11-25)のセキュリティのベストプラクティスにも、すでにこう書かれていました。
MCP Servers MUST NOT use sessions for authentication.
セッションIDは番号札であって、身分証ではありません。仕様は最初からそう言っていて、Grafana の穴は「認証をしろ」を実装しなかったことで生まれました。仕様のバージョンを上げても、自分の実装が変わらなければ穴はそのままです。
新しく増えた「預けた書類」
では、3行目の requestState とは何か。
2026-07-28 版では、サーバーが処理の途中でユーザーに聞き返す方法が変わりました(MRTR: Multi Round-Trip Requests)。サーバーは返事を待たずに、「足りない情報」と「書きかけの書類」をクライアントに返して、いったん処理を終えます。クライアントは答えを集めたら、書類と一緒に同じリクエストを送り直します。
この書きかけの書類が requestState です。サーバーが何も覚えていなくても、送り直されたリクエストを見れば続きから処理できます。
便利な一方で、書類はクライアントの手を通って戻ってきます。仕様も、サーバーはこれを「攻撃者が書き換えうる入力」として扱え(MUST)と書いています。書き換えられて戻ってきたら何が起きるのか、試してみます。
手元で4つの攻撃を再現する
確かめるために、小さなデモサーバーを作りました。コードは公開しています。
ツールは delete_file 一つだけです。sandbox/<ユーザー名>/ の中のファイルを、確認を取ってから消します。MCP の SDK は使わず、標準ライブラリだけで MRTR の往復を書いています。認証(Bearer トークン)は最初から入れてあります。Grafana の教訓は済ませた状態で、その先に何が残るかを見るためです。
1回目の tools/call で、サーバーはこう返します。
{
"resultType": "input_required",
"inputRequests": {
"confirm": {
"method": "elicitation/create",
"params": { "mode": "form", "message": "a.txt を削除してよいですか?", "requestedSchema": {"type": "object", "properties": {}} }
}
},
"requestState": "eyJwYXRoIjogImEudHh0IiwgInVzZXIiOiAiYWxpY2UifQ"
}
最初のサーバー(--mode vulnerable)は、書類を base64 にしただけです。中身を戻すと {"path": "a.txt", "user": "alice"} が出てきます。
攻撃1は、この path を1か所書き換えるだけです。
forged = tamper(state, path="important.txt") # a.txt → important.txt
call("token-alice", 2, {"path": "a.txt"}, forged, ACCEPT)
ユーザーが承認したのは a.txt です。消えたのは important.txt でした。クライアントがプロンプトインジェクションなどで乗っ取られていれば、この書き換えは十分に起こりえます。
残りの3つも試しました。python3 attack.py で4つ全部が順に走ります。
| 攻撃 | 何をするか | vulnerable の結果 |
|---|---|---|
| 1 | 書類の path を書き換える |
削除しました: alice/important.txt |
| 2 | 使い終わった書類を、期限が切れたあとにもう一度使う | 削除しました: alice/a.txt |
| 3 | mallory が自分の書類の user を alice に書き換える |
削除しました: alice/important.txt |
| 4 | alice の正規の書類(改ざんなし)を mallory がそのまま使う | 削除しました: alice/important.txt |
4つとも通りました。認証は入れてあるので、mallory も正規のユーザーです。それでも、預けた書類を信じた時点で、他人のファイルまで消せてしまいます。
割印を押して防ぐ
直し方は、銀行の書類と同じです。書類に割印を押して、戻ってきたときに確かめます。押すときに、次の3つも書類の中に書いておきます。
- 誰の書類か: 認証したユーザー名
- いつまで有効か: 有効期限
- どの手続きの書類か: メソッド名と引数のハッシュ
--mode signed の書類を作る部分です。
def make_state(user, method, args):
payload = {"path": args["path"], "user": user,
"exp": int(time.time()) + STATE_TTL, # 有効期限
"req": args_digest(method, args)} # どの手続きか
body = b64e(json.dumps(payload).encode())
sig = b64e(hmac.new(SECRET, body.encode(), hashlib.sha256).digest()) # 割印
return f"{body}.{sig}"
戻ってきたときに確かめる部分です。
def read_state(state, user, method, args):
body, sig = state.split(".")
expected = b64e(hmac.new(SECRET, body.encode(), hashlib.sha256).digest())
if not hmac.compare_digest(sig, expected):
raise PermissionError("割印が合わない (書き換えられている)")
payload = json.loads(b64d(body))
if payload["exp"] < time.time():
raise PermissionError("有効期限切れ")
if payload["user"] != user: # user は認証から取る。書類からは取らない
raise PermissionError("別のユーザーの書類")
if payload["req"] != args_digest(method, args):
raise PermissionError("別の手続きの書類")
return payload
鍵(SECRET)はサーバーの中だけに置きます。クライアントは書類の中身を読めても、鍵がないので割印を作り直せません。
同じ4つの攻撃を当て直した結果です。
| 攻撃 | vulnerable | signed |
|---|---|---|
1. path を書き換える |
削除しました: alice/important.txt | 拒否: 割印が合わない (書き換えられている) |
| 2. 期限切れの書類を再利用 | 削除しました: alice/a.txt | 拒否: 有効期限切れ |
3. user を他人にする |
削除しました: alice/important.txt | 拒否: 割印が合わない (書き換えられている) |
| 4. 他人の正規の書類を使う | 削除しました: alice/important.txt | 拒否: 別のユーザーの書類 |
4つとも止まりました。攻撃4を止めたのは「誰の書類か」の確認です。改ざんしていない本物の書類なので、割印は合ってしまいます。3つの確認は、どれも役目が違います。
ただし、止められなかったものが一つあります。有効期限内に同じ書類をもう一度送ると、signed でも2回目の削除が通りました(デモの期限は3秒です)。
これは仕様どおりの限界です。仕様も、署名と期限だけでは「1回しか使えない」ことは保証できないと書いています。送金や削除のように1回限りにしたい操作は、使用済みの書類をサーバー側で記録する必要があります。サーバーを何台も並べているなら、その記録は全台で共有しなければなりません。ステートレスにしたはずのサーバーに、ここだけ状態が戻ってくるのは少し皮肉です。
自分のサーバーで確認する3点
-
認証が本当に有効になっているか。mcp-grafana なら v1.1.0 以降に上げたうえで、
--server-auth-tokenを設定しているか。「認証機能がある」と「認証が効いている」は別物です -
サーバーの外に出した状態に、割印を押しているか。
requestStateに限らず、クライアントを経由して戻ってくる値はすべて対象です。中に「誰の・いつまで・どの手続きの」を入れて確かめます - 1回限りの操作に、使用済みの記録があるか。署名と期限だけでは、期限内の再送は止まりません
まとめ
Grafana の番号札は、形しか見られていませんでした。2026-07-28 版で番号札(セッションID)はなくなりました。代わりに増えたのが、預けた書類(requestState)です。番号札の形だけ見て通すのも、預けた書類をそのまま信じるのも、失敗としては同じです。
仕様は守るべきことを書いてくれますが、守るのは自分の実装です。あなたのサーバーは、戻ってきた書類の割印を確かめていますか。
MCP サーバーの認証設計、OWASP MCP Top 10 を踏まえた攻撃と対策は、書籍『MCP実践セキュリティ AIエージェント時代のModel Context Protocol安全運用ガイド』(インプレス、10月16日発売)にまとめました。
Kindle版 / ペーパーバック版
参考
- Pillar Security (Ariel Fogel ほか), Valid but never issued: session spoofing and SSRF in Grafana MCP
- Grafana Labs, Security advisory CVE-2026-19516
- Grafana Labs, mcp-grafana v1.1.0 release notes
- Model Context Protocol, Specification 2026-07-28: Multi Round-Trip Requests
- Model Context Protocol, Specification 2025-11-25: Security Best Practices
- デモコード: kenimo49/mcp-requeststate-demo


