MCP Object-Authz Lab

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.

Category
Visit Server

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_get is 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_id argument 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/list are scoped but update/delete/transfer slipped 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. list only returns your org's objects, so ids feel "private" — but get/delete accept any id and the ids are guessable or enumerable.
  • [ ] Create/update accepting a foreign parent. create(parent_id=…) or a re-parent on update accepts 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.

Recommended Servers

playwright-mcp

playwright-mcp

A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.

Official
Featured
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.

Official
Featured
Local
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.

Official
Featured
Local
TypeScript
VeyraX MCP

VeyraX MCP

Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.

Official
Featured
Local
graphlit-mcp-server

graphlit-mcp-server

The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.

Official
Featured
TypeScript
Kagi MCP Server

Kagi MCP Server

An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.

Official
Featured
Python
E2B

E2B

Using MCP to run code via e2b.

Official
Featured
Neon Database

Neon Database

MCP server for interacting with Neon Management API and databases

Official
Featured
Exa Search

Exa Search

A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.

Official
Featured
Qdrant Server

Qdrant Server

This repository is an example of how to create a MCP server for Qdrant, a vector search engine.

Official
Featured