MCP Object-Authz Lab
Multi-tenant MCP note server for practicing object-level authorization (BOLA/IDOR) bugs across 13 vulnerable/fixed scenarios.
Identify the session user and organization from a token with
whoami.List notes in your organization and get, create, update, or delete notes by id.
Search notes by substring, with an optional
org_idscope parameter.Batch-fetch notes by id and export organization notes, including a wildcard global-export option.
Use cross-organization/admin-oriented tools such as
note_admin_getandnote_create_in_org.Fetch a note through a filtered store query with
note_get_by_query.Prepare and redeem share grants for notes.
Use HTTP-transport features:
X-Org-Id-scoped note listing and quota-limited note creation.Batch-resolve up to 50 notes and fetch notes by the token scope claim.
Read note resources via
note://{token}/{orgId}/{noteId}.Toggle scenarios between vulnerable and fixed builds, run the PoC/tests, and practice detection/audit workflows.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@MCP Object-Authz Lab run the cross-tenant delete exploit"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
MCP Object-Authz Lab
A vulnerable-by-design MCP server for practising object-level authorization bugs (BOLA / IDOR, CWE-639, CWE-862) in MCP tools. Thirteen hands-on scenarios, each one a different variant of the bug, each with a vulnerable build and a fixed build.
git clone https://github.com/WRG-11/mcp-objauthz-lab && cd mcp-objauthz-lab
npm install && npm run poc # Node.js 20 or newerThe PoC is an MCP client. It starts the server on your machine and sends the same cross-tenant request to both builds. Two rows of its 38-row output:
SC BUILD ACTION OUTCOME EXPECT OK
S2 vuln note_search scope-as-param (Alice→org_globex) LEAKED LEAKED ✓
S2 fixed note_search scope-as-param (Alice→org_globex) SCOPED SCOPED ✓
Two-way gate: PASS (38/38 rows OK)Same caller, same request: the vulnerable build returns another tenant's notes, the fixed build scopes the result to the caller's own tenant.
Try the scenarios without the answers · Walkthroughs · Hunt checklist
Why this lab exists
Most MCP security attention goes to prompt injection and tool poisoning. Object-level authorization is a 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 a caller in one tenant can read or change another tenant's objects.
This is not theoretical. Asana's MCP connector (Jun 2025) leaked data across tenant boundaries for roughly 1,000 customer organizations (Pomerium's writeup). In n8n-mcp, CVE-2026-54052 let any caller read or delete another tenant's stored API keys: sequential integer ids on a table with no tenant-id column. Both are S1/S3-shaped bugs, in production instead of a lab.
Related MCP server: vuln-mcp-server
Try the challenges
The scenarios are in challenges/, with no hints until you
open solutions/. Each runs locally in under 5 minutes.
Scenario | Pattern |
Inconsistent authorization: find the single outlier | |
Client-supplied scope trusted as authorization | |
List→get asymmetry: batch skips per-object check | |
Wildcard/sentinel value bypasses scope filter | |
Role/token-type bypass: admin-named tool, no role check | |
Foreign-parent injection: create trusts a caller-supplied org | |
Unscoped query: tenant key omitted from the filter (the real-world shape) | |
Resource-URI-as-scope: the resources/read surface, not tools/call | |
Authz-from-client-round-tripped-value: an editable share grant | |
Forwarded-header-as-scope: a trusted request header (HTTP transport) | |
X-Forwarded-For quota bypass: a trusted request header for rate limiting (HTTP transport) | |
Batch/bulk endpoint BOLA: multi-object endpoint without per-item tenant filter | |
JWT/token scope confusion: token's scope/aud claim vs actual permissions mismatch |
Walkthroughs with hints and answers are in docs/scenarios.md.
Quickstart (< 5 minutes)
Requirements: Node.js 20 or later.
npm install
npm test # 92 tests: unit, docs-consistency, CI-hardening, PoC-output and fixture-annotation checks
npm run poc # 38-row two-way gate: the tools/resources wired end-to-end over MCP
npm run poc -- --json-output poc-chain-evidence.json --sarif-output poc-chain-evidence.sarifExpected npm run poc output (38/38 rows, all scenarios + the hardened build):
MCP object-level authorization lab — two-way gate (13 scenarios + hardened build)
SC BUILD ACTION OUTCOME EXPECT OK
S1 vuln note_get cross-tenant (Bob→Acme) DENIED DENIED ✓
S1 vuln note_delete cross-tenant (Bob→Acme) DELETED DELETED ✓
S1 fixed note_delete cross-tenant (Bob→Acme) DENIED DENIED ✓
S1 fixed note_delete same-tenant (Bob→Globex) DELETED DELETED ✓
S2 vuln note_search scope-as-param (Alice→org_globex) LEAKED LEAKED ✓
S2 fixed note_search scope-as-param (Alice→org_globex) SCOPED SCOPED ✓
S3 vuln note_batch_get list→get asymm (Alice+Globex) LEAKED LEAKED ✓
S3 fixed note_batch_get list→get asymm (Alice+Globex) SCOPED SCOPED ✓
S4 vuln note_export wildcard org_id='*' (Alice) LEAKED LEAKED ✓
S4 fixed note_export wildcard org_id='*' (Alice) SCOPED SCOPED ✓
S5 vuln note_admin_get cross-org as Bob (user) LEAKED LEAKED ✓
S5 fixed note_admin_get cross-org as Bob (user) DENIED DENIED ✓
S5 fixed note_admin_get cross-org as Dana (real admin) ALLOWED ALLOWED ✓
S6 vuln note_create_in_org org_id=org_globex (Alice) INJECTED INJECTED ✓
S6 fixed note_create_in_org org_id=org_globex (Alice) SCOPED SCOPED ✓
S7 vuln note_get_by_query cross-tenant (Alice→Globex) LEAKED LEAKED ✓
S7 fixed note_get_by_query cross-tenant (Alice→Globex) DENIED DENIED ✓
S8 vuln resources/read cross-tenant (Alice→Globex) LEAKED LEAKED ✓
S8 fixed resources/read cross-tenant (Alice→Globex) DENIED DENIED ✓
S9 vuln note_share_redeem tampered grant (Alice→Globex) LEAKED LEAKED ✓
S9 fixed note_share_redeem tampered grant (Alice→Globex) DENIED DENIED ✓
S9 fixed note_share_redeem own grant (Alice→Acme) ALLOWED ALLOWED ✓
S10 vuln note_get_scoped X-Org-Id=org_globex (Alice over HTTP) LEAKED LEAKED ✓
S10 fixed note_get_scoped X-Org-Id=org_globex (Alice over HTTP) SCOPED SCOPED ✓
S11 vuln note_create_limited quota exhausted (same XFF) BLOCKED BLOCKED ✓
S11 vuln note_create_limited quota key is 1.2.3.4 MATCH MATCH ✓
S11 vuln note_create_limited fresh server XFF=5.6.7.8 (quota key logic) CREATED CREATED ✓
S11 vuln note_create_limited quota exhausted XFF=5.6.7.8 BLOCKED BLOCKED ✓
S11 fixed note_create_limited quota exhausted (same XFF) BLOCKED BLOCKED ✓
S11 fixed note_create_limited quota key is u_alice MATCH MATCH ✓
S11 fixed note_create_limited fresh server XFF=5.6.7.8 (quota key logic) CREATED CREATED ✓
S11 fixed note_create_limited quota exhausted XFF=5.6.7.8 BLOCKED BLOCKED ✓
S12 vuln note_batch_resolve batch with Globex id (Alice) LEAKED LEAKED ✓
S12 fixed note_batch_resolve batch with Globex id (Alice) SCOPED SCOPED ✓
S13 vuln note_get_by_token_scope scope=org_globex (Alice) LEAKED LEAKED ✓
S13 fixed note_get_by_token_scope scope=org_globex (Alice) SCOPED SCOPED ✓
ALL fixed 13 cross-tenant routes (Bob→Acme) BLOCKED BLOCKED ✓
ALL fixed legitimate access (Dana admin + Bob own note) ALLOWED ALLOWED ✓
Two-way gate: PASS (38/38 rows OK)The PoC is a real MCP client. It spawns the server over stdio, locally, with no
network and no third party, and runs a two-way gate per scenario: in the vuln
build the exploit succeeds; in the fixed build it is blocked and legitimate
same-org access still works (no false positive).
--json-output writes the structured evidence contract used by the optional
toolkit adapter. --sarif-output writes only observed vulnerability outcomes
from the intentionally vulnerable build; fixed controls and expected denials
are not emitted as findings.
The final ALL rows apply that same two-way discipline to the whole server at
once. With every scenario fixed, each cross-tenant route is closed while legitimate
access (an admin's cross-org read, a user's own note) still works. Each S1-S13
arm deliberately pins one toggle and leaves the rest at their vuln default, so
without these rows the hardened build that Run modes tells you to run
would have no coverage at all.
Detection rules
detection/ ships 50 Semgrep
rules that flag these patterns in your own MCP server source, not just this
lab's. There is one per code shape above, with Python siblings where the
JavaScript spelling cannot parse as Python, plus Go, Rust, Kotlin, Java, Ruby, PHP, C#,
and Swift language packs. Five additional rules cover file paths, dynamic code
execution, and outbound requests. Eleven of the object-authz rules run against Python as well as
JavaScript/TypeScript, which matters because the reference MCP SDKs ship in both.
Measured against this lab's own source on 2026-10-28, the rules report a line in the
handlers of 9 of the 13 scenarios (S2, S4, S6, S7, S8, S9, S10, S11, S13). The other
four have their fix written in the same handler behind a runtime toggle, which the
rules deliberately read as present.
An earlier count listed S1 as flagged; that finding was on note_get, a correctly
authorized handler, and is fixed. The 45-rule revision measured on 2026-10-28
produced zero findings against the official @modelcontextprotocol/sdk, with a
planted canary proving the scan reached the tree. The five new rules require a
new two-way scan before making the same claim. The per-scenario breakdown is in detection/README.md
and the measurement notes are in docs/detection-notes.md.
Drop it into your own MCP server's CI as a GitHub Action. Findings upload to
your repo's Security tab, so the calling workflow needs
security-events: write:
permissions:
security-events: write # only needed for the SARIF upload
steps:
- uses: WRG-11/mcp-objauthz-lab@main
with:
path: src/ # your MCP server sourceAudit a whole MCP server
Object-level authorization remains the ruleset's main focus. skills/mcp-audit/
is a Claude Code skill that reviews the
rest of a server's tool surface as well: how each tool describes itself, whether
result shapes and errors agree across tools, which tools have tests, which look
unused, and how state is written. On a multi-tenant server it runs these rules
and walks the hunt checklist as one of its steps. It is read-only and writes a
single report with a severity per finding.
Run modes
The lab is a multi-tenant note server exposing eighteen MCP tools and one MCP resource
across thirteen independent BOLA scenarios. Each scenario has its own toggle, LAB_S1
to LAB_S13, and every toggle defaults to vuln. Run all scenarios in their fixed state:
# Linux / macOS
LAB_S1=fixed LAB_S2=fixed LAB_S3=fixed LAB_S4=fixed LAB_S5=fixed LAB_S6=fixed LAB_S7=fixed LAB_S8=fixed LAB_S9=fixed LAB_S10=fixed LAB_S11=fixed LAB_S12=fixed LAB_S13=fixed npm start
# Windows PowerShell
$env:LAB_S1='fixed'; $env:LAB_S2='fixed'; $env:LAB_S3='fixed'; $env:LAB_S4='fixed'; $env:LAB_S5='fixed'; $env:LAB_S6='fixed'; $env:LAB_S7='fixed'; $env:LAB_S8='fixed'; $env:LAB_S9='fixed'; $env:LAB_S10='fixed'; $env:LAB_S11='fixed'; $env:LAB_S12='fixed'; $env:LAB_S13='fixed'; npm startWhat each toggle changes, and how to isolate one scenario, is in
docs/configuration.md.
More documentation
docs/scenarios.md: one walkthrough per scenario.docs/hunt-checklist.md: what to read when auditing a real multi-tenant MCP server.docs/architecture.md: the file map and the identity model.docs/configuration.md: the environment variables.docs/detection-notes.md: what the rules were measured to catch and miss.skills/mcp-audit/: the agent skill that audits a whole MCP server.
Scenario reference
Rule messages link to the headings below. Each one points to its challenge and walkthrough.
Scenario S1 — Missing org-scope check (the original)
Inconsistent authorization: find the single outlier. note_delete, CWE-639 / CWE-862, toggle LAB_MODE / LAB_S1.
Challenge, walkthrough.
Scenario S2 — Scope-as-param
Client-supplied scope trusted as authorization. note_search, CWE-639, toggle LAB_S2.
Challenge, walkthrough.
Scenario S3 — List→get asymmetry
List→get asymmetry: batch skips per-object check. note_batch_get, CWE-862, toggle LAB_S3.
Challenge, walkthrough.
Scenario S4 — Wildcard/sentinel bypass
Wildcard/sentinel value bypasses scope filter. note_export, CWE-639, toggle LAB_S4.
Challenge, walkthrough.
Scenario S5 — Role/token-type bypass
Role/token-type bypass: admin-named tool, no role check. note_admin_get, CWE-863, toggle LAB_S5.
Challenge, walkthrough.
Scenario S6 — Foreign-parent injection
Foreign-parent injection: create trusts a caller-supplied org. note_create_in_org, CWE-639, toggle LAB_S6.
Challenge, walkthrough.
Scenario S7 — Unscoped query
Unscoped query: tenant key omitted from the filter (the real-world shape). note_get_by_query, CWE-639, toggle LAB_S7.
Challenge, walkthrough.
Scenario S8 — Resource-URI-as-scope
Resource-URI-as-scope: the resources/read surface, not tools/call. note://{token}/{orgId}/{noteId}, CWE-639, toggle LAB_S8.
Challenge, walkthrough.
Scenario S9 — Authz-from-client-round-tripped-value
Authz-from-client-round-tripped-value: an editable share grant. note_share_prepare, note_share_redeem, CWE-639, toggle LAB_S9.
Challenge, walkthrough.
Scenario S10 — Forwarded-header-as-scope
Forwarded-header-as-scope: a trusted request header (HTTP transport). note_get_scoped, CWE-639 / CWE-290, toggle LAB_S10.
Challenge, walkthrough.
Scenario S11 — Forwarded-header quota bypass
X-Forwarded-For quota bypass: a trusted request header for rate limiting (HTTP transport). note_create_limited, CWE-639 / CWE-290, toggle LAB_S11.
Challenge, walkthrough.
Scenario S12 — Batch resolve without a per-item check
Batch/bulk endpoint BOLA: multi-object endpoint without per-item tenant filter. note_batch_resolve, CWE-639 / CWE-862, toggle LAB_S12.
Challenge, walkthrough.
Scenario S13 — Token scope claim trusted as permission
JWT/token scope confusion: token's scope/aud claim vs actual permissions mismatch. note_get_by_token_scope, CWE-639 / CWE-290, toggle LAB_S13.
Challenge, walkthrough.
Safety / scope
This server is vulnerable by design. Do not deploy it on a reachable network or use it as a starting point for real code. Run it locally for learning only.
Everything in it is synthetic. The orgs and users are made up, and so are their notes and tokens; there is no real target. The PoC never makes a network request. It only spawns the local server process over stdio.
Contributing
The most useful contribution to a detection ruleset is a false positive:
a rule that fires on correctly authorized code. A gate wider than the defect
it targets gets switched off, and a switched-off rule protects nothing, so
those are treated as real defects here. Misses are just as welcome; the rules
flag 9 of the 13 scenarios against this lab's own source and
detection/README.md says why.
There is an issue template for each. CONTRIBUTING.md has
the fixture convention and the exact-count CI gate. It also describes the
multi-language rule trap that costs an afternoon if you meet it the hard way.
Participation is covered by the Code of Conduct.
Security reports go through SECURITY.md. Please do not
report the planted flaws; they are the point.
Citing this
CITATION.cff, or use GitHub's "Cite this repository" button.
License
MIT.
Available Tools
18 toolsnote_admin_getA
Get any note by id, across organizations. Admin/support use only.
Inputs: id (required), token (required).
note_batch_getA
Fetch multiple notes by id in a single call (up to 20 ids).
Inputs: ids (required), token (required).
note_batch_resolveC
Fetch multiple notes by id in a single batch call (up to 50 ids). Returns all requested notes.
Inputs: ids (required), token (required).
note_createC
Create a note in your organization.
Inputs: title (required), token (required), body.
note_create_in_orgA
Create a note inside a specific organization (cross-team collaboration). The org_id parameter targets the destination org.
Inputs: title (required), token (required), body, org_id.
note_create_limitedA
Create a note with a per-client quota (max 3 notes). The quota is tracked by client identity.
Inputs: title (required), token (required), body.
note_deleteC
Delete a note by id.
Inputs: id (required), token (required).
note_exportB
Export all notes for an organization. Pass org_id='*' for a global export (admin only).
Inputs: token (required), org_id.
note_getC
Get one note by id.
Inputs: id (required), token (required).
note_get_by_queryC
Fetch a single note by id, resolved through a filtered store query.
Inputs: id (required), token (required).
note_get_by_token_scopeB
Get notes for the organization indicated by the token's scope claim. The token carries the intended scope.
Inputs: token (required), scope.
note_get_scopedB
List the caller's notes. Honors an X-Org-Id routing header set by the API gateway.
Inputs: token (required).
note_listC
List the notes in your organization.
Inputs: token (required).
note_searchB
Search notes by substring. The optional org_id parameter restricts the scope (admin use).
Inputs: q (required), token (required), org_id.
note_updateA
Update a note's body by id.
Inputs: id (required), body (required), token (required).
whoamiA
Return the session (user + org) your token authenticates as.
Inputs: token (required).
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 18 tools
There are many overlapping read operations: note_get, note_get_by_query, note_get_scoped, note_get_by_token_scope, note_admin_get, and two batch getters. note_batch_resolve and note_batch_get especially are nearly indistinguishable, so an agent would struggle to pick the right tool for a simple fetch.
Most tools follow a consistent note_<action> pattern, e.g. note_list, note_get, note_create, note_update, note_delete. Minor deviations like whoami and the inconsistent note_batch_resolve versus note_batch_get keep it from being a perfect 5.
18 tools is above the comfortable 3-15 range and feels heavy for a notes/authz lab. Many read variants inflate the count when fewer, more distinct tools could express the same authorization scenarios.
The toolset covers the core note lifecycle: create, get, list, search, update, delete, export, plus authz-specific operations like admin access, org-scoped creation, token-scoped reads, and sharing. Minor gaps like share revocation and scoped update/delete variants exist, but core workflows are not dead-ended.
Maintenance
Related MCP Connectors
Security research canary remote MCP server for owned-account testing.
MEOK MCP Hardening MCP — automated security red-team for any MCP server. Maps OWASP LLM Top 10
A public MCP honeypot: a fake ops console with fake secrets. Every call is shown on decoy.day.
Self-hosted federated MCP gateway: one OAuth 2.1 MCP server in front of N apps, user-level scopes.
Related MCP Servers
- FlicenseNot gradedqualityDmaintenanceA deliberately insecure MCP server designed as a pentest lab to demonstrate common vulnerabilities in MCP deployments.-
- FlicenseNot gradedqualityDmaintenanceA vulnerable-by-design MCP server pair (NotesServer and VaultServer) for testing MCP security tools, featuring confused-deputy, prompt injection, and authorization bypass scenarios.-
- FlicenseNot gradedqualityDmaintenanceA vulnerable MCP server designed for educational CTF challenges. It demonstrates various MCP security vulnerabilities in a controlled environment.8-
- FlicenseNot gradedqualityCmaintenanceAn intentionally vulnerable MCP server for security education, demonstrating flaws like missing auth, SSRF, SQLi, and file system abuse.-