MCP Object-Authz Lab
A self-hostable, vulnerable-by-design MCP server for learning how object-level authorization bugs (BOLA/IDOR) appear in multi-tenant tools.
README
MCP Object-Authz Lab
A small, self-hostable, vulnerable-by-design MCP server for learning how object-level / cross-tenant authorization bugs (BOLA / IDOR — CWE-639, CWE-862) appear in Model Context Protocol tools, and how to hunt them.
It is a multi-tenant note server exposing six MCP tools. Five are correctly authorized. Exactly one is missing its access check, so a caller in one organization can reach another organization's data through it. The bug is subtle on purpose: the broken tool looks just like its siblings — it is missing a single line. Find it, exploit it locally, then flip one switch to see it fixed.
Why this lab exists
Most MCP security attention goes to prompt injection and tool-poisoning. Object-level authorization is a quieter, different class, and the usual prompt-injection test suites and scanners do not find it. When an MCP server is multi-tenant, every tool that resolves an object from a client-supplied id must verify the caller is allowed to touch that object. Miss the check on a single tool and you have a cross-tenant read, write, or delete — regardless of how good the prompt-injection defenses are. Catching it takes reading the authorization on each tool, which is exactly the muscle this lab trains.
Quickstart (< 5 minutes)
Requirements: Node.js ≥ 20.
npm install
npm run poc
Expected output:
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).
The PoC is a real MCP client. It spawns the server over stdio (locally — no network, no third party), authenticates as Bob (org Globex), and tries to reach a note owned by Acme. It runs a two-way gate:
- in the vuln build the cross-tenant delete succeeds (the exploit), while
the correctly-scoped
note_getis still denied — proving the server has a single broken tool, not a globally missing check; - in the fixed build the same cross-tenant delete is blocked, and Bob deleting his own org's note still works — proving the fix does not over-block (no false positive).
The challenge
Before you read src/: the server exposes six note tools —
note_list, note_get, note_create, note_update, note_delete,
note_search. Exactly one lets a caller in one org affect another org's
note. Which one, and what makes it different from the others?
<details> <summary>Hint</summary>
Three tools take a client-supplied id (note_get, note_update,
note_delete). Two of them check that the resolved object belongs to your org.
One does not.
</details>
<details> <summary>Answer</summary>
note_delete. It resolves the note from the client-supplied id exactly like
note_get and note_update, but (in LAB_MODE=vuln) it never calls
requireOrgAccess(session, note). The fix is that one line — the same check its
siblings already perform. See the comment block in
src/tools.js above note_delete.
</details>
How it is built
| File | Role |
|---|---|
src/store.js |
In-memory multi-tenant seed data: orgs Acme (Alice) and Globex (Bob), a few notes each. |
src/auth.js |
resolveSession(token) → server-trusted { user, org }; requireOrgAccess(session, object) — the object-level check. |
src/tools.js |
The six tools. note_delete is the planted outlier. |
src/server.js |
Stdio MCP server. Reads LAB_MODE (vuln default / fixed). |
poc/exploit.js |
MCP client running the two-way gate above. |
Identity model (a deliberate simplification). Each tool takes a bearer
token that the server resolves to a fixed user and org. The caller never
asserts its own org — only presents a token. In a production MCP server this
identity would come from the transport / OAuth layer rather than a per-call
argument; the lab passes it per call so it stays a single process and the
authorization logic is explicit and easy to read.
Vulnerable vs fixed
One environment variable toggles the planted bug, so you can run either build in your own MCP host:
# vulnerable (default)
npm start
# or: LAB_MODE=vuln npm start
# fixed
LAB_MODE=fixed npm start
On Windows PowerShell:
$env:LAB_MODE = 'fixed'; npm start
The only difference between the two builds is the single line marked
// <-- THE FIX in note_delete.
Hunt checklist — object-level authorization in MCP
Use this when auditing a real multi-tenant MCP server (one you own or are authorized to test). The bug class is "the server authenticates who you are but forgets to check whether you may touch this object":
- [ ] Client-supplied scope trusted as authorization. A tool takes an
org_id/project_id/tenant_id/account_idargument and uses it to scope the query instead of checking it against the caller's membership. - [ ] Membership check decoupled from object resolution. The tool verifies
the caller belongs to some org/project, but loads the object by a different
id (the object's own
id) without confirming the object lives under that membership. (Authorize the object you are about to return/mutate — not a parameter next to it.) - [ ] Inconsistent authorization — the single outlier. Most object tools
check; one or two do not. Read every tool that resolves an object by id,
not a sample. The forgotten one is usually a less-glamorous verb (
delete,update,archive,export, a "cover"/"make-default" side action). - [ ] Reads guarded, mutations not.
get/listare scoped butupdate/delete/transferslipped through — or vice-versa. - [ ] Wildcard / sentinel short-circuit. A special value
(
'all','*', empty,0,null) skips the scope filter entirely. - [ ] Role / token-type bypass. An "internal", "service", "admin", or alternate-JWT-type code path skips the per-object check.
- [ ] List → get asymmetry.
listonly returns your org's objects, so ids feel "private" — butget/deleteaccept any id and the ids are guessable or enumerable. - [ ] Create/update accepting a foreign parent.
create(parent_id=…)or a re-parent onupdateaccepts a parent the caller is not a member of, injecting an object into another tenant.
The exploit primitive is always the same: authenticate as tenant B, call the suspect tool with an object id that belongs to tenant A, and see whether you get A's data (or mutate it). Confirm a fix the same way the PoC here does — two-way: the cross-tenant call must be blocked and the legitimate same-tenant call must still succeed.
Safety / scope
- Vulnerable by design. Do not deploy this on a reachable network or use it as a starting point for real code. Run it locally for learning only.
- Synthetic. All orgs, users, notes, and tokens are made up. There is no real data, no real target, and the PoC never makes a network request — it only spawns the local server process.
License
MIT.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。