🤖 MCP オブジェクト認可ラボ
小規模でセルフホスト可能な 設計上脆弱 (vulnerable-by-design) な MCP サーバー。Model Context Protocol のツール群において オブジェクトレベル/クロステナント認可 の脆弱性(BOLA / IDOR — CWE-639, CWE-862)がどのように現れるか、そしてそれをどう発見するかを学ぶための教材です。
マルチテナント構成のノートサーバーで、6 つの MCP ツールを公開しています。うち 5 つは正しく認可されていますが、ちょうど 1 つだけ アクセスチェックが欠落しているため、ある組織の呼び出し元が別の組織のデータに到達できてしまいます。このバグは意図的に巧妙に仕込まれています。壊れているツールは兄弟ツールと見た目が同じで、欠けているのはたった 1 行です。それを見つけ、ローカルで悪用し、スイッチを 1 つ切り替えて修正される様子を確認してください。
🎯 このラボの目的
MCP のセキュリティは主に プロンプトインジェクション やツールポイズニングに注目が集まりがちです。オブジェクトレベル認可はより静かで、まったく別の種類の問題であり、一般的なプロンプトインジェクションのテストスイートやスキャナーでは 発見できません。MCP サーバーがマルチテナントである場合、クライアント供給の id からオブジェクトを解決するすべてのツールは、呼び出し元が その オブジェクトに触れる権限があるかを検証しなければなりません。1 つのツールでこのチェックを怠ると、クロステナントの読み取り・書き込み・削除が可能になります — プロンプトインジェクション対策がどれほど優れていても関係ありません。これを見つけるには 各ツールの認可を読み解く ことが必要であり、まさにこのラボが鍛えるスキルです。
🚀 クイックスタート(5 分以内)
要件:Node.js ≥ 20
npm install
npm run poc
期待される出力:
MCP object-level authorization lab — two-way gate
BUILD ACTION OUTCOME EXPECT OK
vuln note_get cross-tenant (Bob → Acme) DENIED DENIED ✓
vuln note_delete cross-tenant (Bob → Acme) DELETED DELETED ✓
fixed note_delete cross-tenant (Bob → Acme) DENIED DENIED ✓
fixed note_delete same-tenant (Bob → Globex) DELETED DELETED ✓
Two-way gate: PASS (vuln exploits, fix blocks the cross-tenant delete and still allows the same-tenant one).
この PoC は実際の MCP クライアントです。stdio 経由でサーバーを起動し(ローカルのみ — ネットワークも第三者もなし)、Bob(所属組織 Globex)として認証し、Acme が所有するノートに到達しようと試みます。いわゆる 双方向ゲート (two-way gate) を実行します:
- vuln ビルドでは、クロステナント削除が 成功 する一方で、正しくスコープされた
note_getは依然として拒否される — これはサーバーが 1 つだけ 壊れたツールを持ち、全体的にチェックが欠けているわけではないことを証明します。 - fixed ビルドでは、同じクロステナント削除が ブロック され、Bob が 自分の 組織のノートを削除する操作は依然として 成功 する — これは修正が過剰ブロック(偽陽性)を引き起こしていないことを証明します。
🧩 チャレンジ
src/ を読む前に:このサーバーは 6 つのノートツールを公開しています —
note_list、note_get、note_create、note_update、note_delete、note_search。ちょうど 1 つ が、ある組織の呼び出し元に別の組織のノートを操作させてしまいます。どれで、他のツールと何が違うのでしょうか?
ヒント
3 つのツールがクライアント供給の id を受け取ります(note_get、note_update、note_delete)。そのうち 2 つは、解決されたオブジェクトが自分の組織に属するかをチェックしています。1 つはチェックしていません。
答え
note_delete です。クライアント供給の id からノートを解決する点は note_get や note_update とまったく同じですが、(LAB_MODE=vuln の場合)requireOrgAccess(session, note) を一切呼び出していません。修正はその 1 行 — 兄弟ツールがすでに実行しているのと同じチェックです。 にある note_delete の上のコメントブロックを参照してください。
🏗️ アーキテクチャ
| ファイル | 役割 |
|---|---|
| インメモリのマルチテナントシードデータ:組織 Acme(Alice)と Globex(Bob)、それぞれいくつかのノート。 | |
resolveSession(token) → サーバー信頼の { user, org };requireOrgAccess(session, object) — オブジェクトレベルのチェック。 |
|
6 つのツール。note_delete が仕込まれた外れ値。 |
|
stdio MCP サーバー。LAB_MODE を読み取る(vuln がデフォルト / fixed)。 |
|
| 上記の双方向ゲートを実行する MCP クライアント。 |
アイデンティティモデル(意図的な簡略化)。 各ツールは bearer token を受け取り、サーバーがそれを固定のユーザーと組織に解決します。呼び出し元が自身の組織を主張することはなく、トークンを提示するだけです。本番の MCP サーバーでは、このアイデンティティは呼び出しごとの引数ではなく、トランスポート/OAuth レイヤーから取得されます。ラボでは、単一プロセスに留め、認可ロジックを明示的で読みやすく保つために、呼び出しごとに渡しています。
🔀 脆弱版と修正版
環境変数 1 つで仕込まれたバグを切り替えられるため、自分の MCP ホストでどちらのビルドでも実行できます:
# vulnerable (default)
npm start
# or: LAB_MODE=vuln npm start
# fixed
LAB_MODE=fixed npm start
Windows PowerShell の場合:
$env:LAB_MODE = 'fixed'; npm start
両ビルドの 唯一 の違いは、note_delete 内の // <-- THE FIX とマークされた 1 行だけです。
✅ 監査チェックリスト — MCP におけるオブジェクトレベル認可
実際のマルチテナント MCP サーバー(自分が所有するか、テストを許可されているもの)を監査する際に使用してください。このバグクラスは「サーバーは あなたが誰か を認証するが、あなたがこのオブジェクトに触れてよいか をチェックし忘れる」というものです:
- [ ] クライアント供給のスコープを認可として信頼している。 ツールが
org_id/project_id/tenant_id/account_id引数を受け取り、それを 呼び出し元のメンバーシップと照合する のではなく クエリのスコープ指定 に使ってしまっている。 - [ ] メンバーシップチェックがオブジェクト解決から分離している。 ツールは呼び出し元が何らかの組織/プロジェクトに属することを検証するが、オブジェクトを 別の id(オブジェクト自身の
id)で読み込み、そのオブジェクトがそのメンバーシップの下に存在することを確認していない。(これから返す/変更するオブジェクトを認可すること — その隣にあるパラメータではなく。) - [ ] 一貫性のない認可 — 唯一の外れ値。 ほとんどのオブジェクト操作ツールはチェックしているが、1 つか 2 つがしていない。サンプルではなく、id でオブジェクトを解決する すべての ツールを読むこと。忘れられがちなのは、たいてい地味な動詞(
delete、update、archive、export、"cover"/"make-default" などのサイドアクション)です。 - [ ] 読み取りは守られているが変更は守られていない。
get/listはスコープされているのにupdate/delete/transferがすり抜けている — あるいはその逆。 - [ ] ワイルドカード/センチネルによるショートサーキット。 特別な値(
'all'、'*'、空、0、null)がスコープフィルタを完全にスキップする。 - [ ] ロール/トークンタイプによるバイパス。 "internal"、"service"、"admin"、または代替 JWT タイプのコードパスが、オブジェクトごとのチェックをスキップする。
- [ ] list → get の非対称性。
listは自分の組織のオブジェクトのみを返すため、id が「プライベート」に感じられる — しかしget/deleteは 任意の id を受け付け、その id は推測可能または列挙可能である。 - [ ] create/update が外部の親を受け入れる。
create(parent_id=…)やupdateでの再親子付けが、呼び出し元がメンバーでない親を受け入れ、別のテナントにオブジェクトを注入してしまう。
攻撃のプリミティブは常に同じです:テナント B として認証し、テナント A に属するオブジェクト id で容疑ツールを呼び出し、A のデータを取得できるか(または変更できるか)を確認します。修正の確認も、この PoC と同じ方法で行ってください — 双方向 で:クロステナント呼び出しがブロックされ、かつ 正当な同一テナント呼び出しが依然として成功する必要があります。
🛡️ 安全性/スコープ
- 設計上脆弱。 到達可能なネットワーク上にデプロイしたり、実際のコードの出発点として使用したり しないでください。学習目的でのみローカルで実行してください。
- 合成データ。 すべての組織、ユーザー、ノート、トークンは架空のものです。実データも実ターゲットも存在せず、PoC はネットワークリクエストを一切行いません — ローカルサーバープロセスを起動するだけです。
📄 ライセンス
MIT.








VIP会員