🔐 MCP 对象级授权实验室
一个小巧、可自托管、设计上即存在漏洞的 MCP 服务器,用于学习 对象级 / 跨租户授权 漏洞(BOLA / IDOR —— CWE-639、CWE-862)如何出现在模型上下文协议(MCP)工具中,以及如何狩猎它们。
它是一个多租户笔记服务器,暴露六个 MCP 工具。其中五个的授权是正确的。恰好有一个缺少访问检查,因此一个组织中的调用者可以通过它访问另一个组织的数据。这个漏洞是被刻意设计得十分隐蔽的:出错的工具看起来和它的同类一模一样——只是少了一行代码。找到它,在本地利用它,然后拨动一个开关,就能看到它被修复。
📖 为什么存在这个实验室
大多数 MCP 安全关注点都集中在 提示注入(prompt injection) 与 工具投毒(tool-poisoning) 上。而对象级授权是一个更安静、且完全不同的类别,常规的提示注入测试套件和扫描器根本发现不了它。
当 MCP 服务器是多租户时,每一个从客户端提供的 id 解析对象的工具,都必须验证调用者是否有权触碰那个对象。只要有一个工具漏掉了这项检查,你就会有跨租户的读取、写入或删除——无论提示注入防御做得多好。要捕获它,只能逐个阅读每个工具上的授权逻辑,而这正是本实验室所训练的能力。
🚀 快速开始(< 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 所有的笔记。它运行一个双向验证门:
- 在 vuln 构建中,跨租户删除成功(即漏洞利用),而正确限定作用域的
note_get仍被拒绝——这证明服务器只有单个工具出错,而非全局缺失检查; - 在 fixed 构建中,同样的跨租户删除被拦截,而 Bob 删除自己组织的笔记仍然可用——这证明修复没有过度拦截(无误报)。
🎯 挑战
在阅读 src/ 之前:服务器暴露了六个笔记工具——note_list、note_get、note_create、note_update、note_delete、note_search。恰好有一个允许某个组织中的调用者影响另一个组织的笔记。是哪一个,它与其他工具的区别又在哪里?
提示
有三个工具接受客户端提供的 id(note_get、note_update、note_delete)。其中两个会检查解析出的对象是否属于你的组织。有一个不会。
答案
note_delete。它完全像 note_get 和 note_update 一样,从客户端提供的 id 解析笔记,但在(LAB_MODE=vuln 时)它从未调用 requireOrgAccess(session, note)。修复就是那一行——与它的同类工具早已执行的检查相同。参见 中 note_delete 上方的注释块。
🏗️ 构建方式
| 文件 | 作用 |
|---|---|
| 内存中的多租户种子数据:组织 Acme(Alice)和 Globex(Bob),各有一些笔记。 | |
resolveSession(token) → 服务器可信的 { user, org };requireOrgAccess(session, object) —— 对象级检查。 |
|
六个工具。note_delete 是植入的异常项。 |
|
Stdio MCP 服务器。读取 LAB_MODE(默认 vuln / fixed)。 |
|
| 运行上文双向验证门的 MCP 客户端。 |
身份模型(一处刻意的简化)。 每个工具都接受一个 bearer token,服务器将其解析为固定的用户与组织。调用者从不声明自己的组织——它只出示一个 token。在生产环境的 MCP 服务器中,这一身份应来自传输层 / OAuth 层,而非每次调用的参数;本实验室逐调用传递它,以保持单进程运行,并让授权逻辑显式且易于阅读。
🔄 漏洞版 vs 修复版
只需一个环境变量即可切换植入的漏洞,因此你可以在自己的 MCP 主机中运行任意一个构建:
# 漏洞版(默认)
npm start
# 或:LAB_MODE=vuln npm start
# 修复版
LAB_MODE=fixed npm start
在 Windows PowerShell 中:
$env:LAB_MODE = 'fixed'; npm start
两个构建之间唯一的差异,就是 note_delete 中标记为 // <-- THE FIX 的那一行。
🔍 审计清单 —— MCP 中的对象级授权
在审计一个真实的多租户 MCP 服务器(你自己拥有的、或被授权测试的)时,可使用本清单。该漏洞类别是:"服务器认证了你是谁,却忘了检查你是否可以触碰这个对象":
- [ ] 把客户端提供的范围当作授权依据。 某工具接受
org_id/project_id/tenant_id/account_id参数,并用它来限定查询范围,而不是将其与调用者的成员资格进行核对。 - [ ] 成员资格检查与对象解析相互脱钩。 工具验证调用者属于某个组织 / 项目,却通过另一个 id(对象自身的
id)加载对象,而未确认该对象确实位于该成员资格之下。(要对你即将返回 / 变更的那个对象授权——而不是它旁边的某个参数。) - [ ] 授权不一致——单个异常项。 大多数对象工具都有检查;一两个没有。要阅读每一个通过 id 解析对象的工具,而非抽样。被遗忘的那个通常是较不起眼的动词(
delete、update、archive、export,或某个 "cover"/"make-default" 之类的附带操作)。 - [ ] 读操作有防护,写操作没有。
get/list限定了作用域,但update/delete/transfer漏掉了——或反之。 - [ ] 通配符 / 哨兵值短路。 某个特殊值(
'all'、'*'、空、0、null)会完全跳过作用域过滤。 - [ ] 角色 / token 类型绕过。 "internal"、"service"、"admin" 或另一种 JWT 类型的代码路径跳过了逐对象检查。
- [ ] list → get 不对称。
list只返回你所在组织的对象,于是 id 显得"私密"——但get/delete接受任意 id,而 id 往往可猜测或可枚举。 - [ ] create/update 接受外部父级。
create(parent_id=…)或update上的重新挂载父级操作,接受了调用者并非成员的父级,从而把对象注入到另一个租户。
漏洞利用原语始终相同:以租户 B 身份认证,用属于租户 A 的对象 id 调用可疑工具,看是否能获得 A 的数据(或变更它)。用本 PoC 一样的方式确认修复——双向:跨租户调用必须被拦截,同时合法的同租户调用必须仍然成功。
🛡️ 安全声明与适用范围
- 设计上即存在漏洞。 切勿将其部署在可访问的网络上,也不要将其作为真实代码的起点。仅在本地运行、用于学习。
- 合成数据。 所有组织、用户、笔记与 token 均为虚构。不存在真实数据、不存在真实目标,且 PoC 从不发起网络请求——它只启动本地服务器进程。
📄 许可证
MIT。








VIP会员