🔎 Last updated · Editorial policy · Report an error
TL;DR: BOLA and IDOR Are Still the #1 MCP Server Risk
MCP tools are just APIs with a conversational front end. If your tool handlers accept object IDs like invoice_id without verifying ownership server-side, any client—LLM-driven or not—can read or mutate other users’ data. Broken object-level authorization didn’t disappear when you bolted an agent onto your backend; it got a new, friendlier interface.
Why MCP Tool Calls Are Just API Calls (and Why Authorization Gets Skipped)
The Model Context Protocol, open-sourced by Anthropic in November 2024 and now stewarded with OpenAI and Google’s involvement in the MCP steering community, defines how LLM clients invoke tools via JSON-RPC 2.0 messages. With the streamable HTTP transport, a tool call looks like this:
POST /mcp HTTP/1.1
Host: localhost:8080
Content-Type: application/json
{
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "get_invoice",
"arguments": {
"invoice_id": "inv_10041"
}
}
}
Strip away the method name and you have a POST endpoint with a JSON body. That’s it. The JSON-RPC envelope is a delivery mechanism, not a security control.
Here’s where the industry keeps making the same mistake. Developers building MCP servers see the LLM client as a trusted intermediary—”the agent knows which user it’s serving”—and skip per-request authorization entirely. You’re not attacking an AI when you exploit these endpoints; you’re attacking a probabilistic reasoning engine wrapped around a plain HTTP API with no authz layer. The agent passes whatever identifiers appear in the conversation straight through to the tool handler, and the handler trusts them.
This is precisely the failure mode OWASP’s Top 10 for LLM Applications warns about with Excessive Agency (LLM06), and it compounds OWASP API Security Top 10 API1:2023—Broken Object Level Authorization. CISA and its international partners made the same point in their March 2025 guidance on securing AI data and agent systems: agent integrations inherit every flaw of the underlying API surface, plus new ones.
BOLA vs IDOR vs Broken Function-Level Authorization in Agent Contexts
Three related terms, three distinct mappings to MCP tool parameters:
- BOLA (API1:2023) — The category. The server fails to verify that the authenticated user owns the object referenced in a tool argument. In MCP terms:
get_customer(customer_id)returns anyone’s customer record. - IDOR — The common manifestation. Predictable or enumerable object references (
inv_10041,user_id=2) let an attacker swap identifiers and receive another user’s data. Same root cause, observable exploit. - BFLA (API5:2023, Broken Function-Level Authorization) — The user can invoke functions they shouldn’t have access to at all. In MCP terms: a standard user calls an
admin_delete_usertool that appears in the tool registry and executes without a role check.
In classic web API testing, you’d hunt these in REST routes. In MCP, the attack surface is the tool registry itself. Enumerate tools via tools/list, read the inputSchema to identify ID-bearing arguments, and you have a complete map of what to fuzz.
Lab Setup: Running a Vulnerable MCP Server Locally
Never test systems you don’t own. Build the lab locally—it takes about five minutes.
Create a deliberately vulnerable MCP server with two users and predictable invoice IDs:
mkdir mcp-bola-lab && cd mcp-bola-lab
python -m venv venv && source venv/bin/activate
pip install "mcp[cli]" uvicorn fastapi
Save this as server.py:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("vulnerable-invoices", host="127.0.0.1", port=8080)
DB = {
"inv_10041": {"owner": "alice", "amount": 4200, "vendor": "CloudCo"},
"inv_10042": {"owner": "bob", "amount": 91000, "vendor": "CryptoMine Ltd"},
"inv_10043": {"owner": "alice", "amount": 310, "vendor": "DesignStudio"},
}
@mcp.tool()
def get_invoice(invoice_id: str) -> dict:
"""Fetch an invoice by ID. No ownership check (vulnerable)."""
return DB.get(invoice_id, {"error": "not found"})
@mcp.tool()
def admin_purge_invoices(confirm: bool) -> dict:
"""Admin-only tool. No role check (vulnerable)."""
if confirm:
DB.clear()
return {"status": "all invoices deleted"}
return {"status": "aborted"}
if __name__ == "__main__":
mcp.run(transport="streamable-http")
Run it: python server.py. The MCP endpoint is now listening on http://127.0.0.1:8080/mcp. Note the flaws: get_invoice trusts the caller-supplied invoice_id with no session binding, and admin_purge_invoices has no role gate. In a real app these would map to per-user authn tokens—here, the absence is the point.
Configuring Burp Suite to Intercept MCP JSON-RPC Traffic
MCP streamable HTTP is plain HTTP, so Burp treats it like any other target:
- In Burp Suite (Community edition works), confirm the proxy listener on
127.0.0.1:8080—change it to:8081here to avoid port collision with the MCP server, or run the MCP server on:9090. - Point your MCP client at the HTTP transport. For a raw client like
mcp-remoteor a custom Python client, setHTTPS_PROXY=http://127.0.0.1:8081andHTTP_PROXY=http://127.0.0.1:8081before launching. Claude Desktop and similar desktop clients using HTTP-based MCP connectors honor standard proxy environment variables; if the endpoint uses TLS, install Burp’s CA certificate (http://burpfrom the proxied browser) into your OS trust store. - Trigger a tool call in the client. Burp’s Proxy history now shows the full JSON-RPC exchange:
initialize,tools/list, andtools/callrequests. - Send a
tools/callrequest to Repeater. This becomes your primary exploitation and testing workbench—you no longer need the LLM client at all. That’s a critical blue-team insight: the agent is optional. Any attacker with network access to the MCP endpoint can craft JSON-RPC directly.
For streamable HTTP, keep the Mcp-Session-Id header from the initialize response in your replayed requests, or the server will reject the call.
Exploit 1: BOLA via a Tool Argument (invoice_id Enumeration)
Alice’s legitimate call for her own invoice returns as expected. In Repeater, swap the object reference to Bob’s invoice:
{
"jsonrpc": "2.0",
"id": 8,
"method": "tools/call",
"params": {
"name": "get_invoice",
"arguments": {"invoice_id": "inv_10042"}
}
}
Expected vulnerable response:
{
"jsonrpc": "2.0",
"id": 8,
"result": {
"content": [{
"type": "text",
"text": "{"owner": "bob", "amount": 91000, "vendor": "CryptoMine Ltd"}"
}]
}
}
The server returned Bob’s confidential invoice to a request that originated from Alice’s session. The amount and vendor fields are now attacker-readable. The payload here is boring—that’s the lesson. BOLA payloads are almost always one swapped identifier, which is why automation (below) catches them so effectively.
Exploit 2: IDOR via Session/User Mismatch (Agent Acting as Another User)
Here’s where MCP diverges from classic web IDOR. In a typical REST API, the session cookie or bearer token identifies the user, and IDOR hinges purely on object references. In agent deployments, a second identity channel creeps in: the conversation itself. Tools like get_invoice_for_user(user_id, invoice_id) accept the user identity as an argument because developers assume the LLM will pass the “correct” user context:
"params": {
"name": "get_invoice_for_user",
"arguments": {
"user_id": "bob",
"invoice_id": "inv_10042"
}
}
The model has no privileged knowledge—it happily accepts user_id: "bob" injected into a prompt, memory, or an upstream document, and forwards it verbatim. This is what makes agent-context IDOR distinct: the identity claim travels through a channel (natural language) that has no cryptographic binding to the actual session. prompt injection plus missing server-side authz means an attacker never needs to touch the API directly—the agent does the forgery for them. Identity must be derived from the transport-layer session on the server, never from tool arguments or model output.
Exploit 3: Function-Level BOLA (Calling Admin Tools as a Normal User)
List the registry first:
{"jsonrpc": "2.0", "id": 9, "method": "tools/list"}
The response advertises admin_purge_invoices to every client. Execute it as a standard user:
{
"jsonrpc": "2.0",
"id": 10,
"method": "tools/call",
"params": {
"name": "admin_purge_invoices",
"arguments": {"confirm": true}
}
}
Response: {"status": "all invoices deleted"}. Full data destruction from an unprivileged session. This maps directly to OWASP API5:2023, and in agent terms it’s Excessive Agency realized: the tool registry exposed a privileged capability with no gate. Fix: never register admin tools in the same server instance (or enforce role checks inside the handler against the session, not the request).
Automating Detection: Burp Intruder and Authz Plugins for Tool Calls
Manual replay proves the flaw; automation proves its scope:
- Intruder for ID fuzzing. Send the
get_invoicerequest to Intruder, markinv_10041as a payload position, and use a numbers or brute-forcer payload set across the ID range. Diff the responses: any response with a differentownerfield than your session’s user is a hit. Length-based sorting makes outliers trivially visible. - Autorize-style checks. The Autorize extension replays every request with a low-privileged session and flags responses that differ—exactly the broken-authorization detection pattern you want. It works on MCP endpoints because they’re ordinary HTTP; filter on
tools/callin the body to cut noise. - Match/filter JSON-RPC. Add a match rule on
"method": "tools/call"in Burp’s proxy scope so only tool invocations hit your test queue.
Fixing It: Server-Side Authorization Patterns for MCP Tools
The defenses are the same ones your API gateway should already enforce—applied per tool:
- Derive identity server-side. Bind the JSON-RPC session (the
Mcp-Session-Id, or the OAuth token per the MCP authorization specification) to a user context at connection time. Tool handlers resolve ownership from that context. Never accept user identifiers as tool arguments. - Object-level checks on every access. Even after fetching by ID, verify
object.owner == session.user(or team/tenant membership) before returning data. Deny by default. - Function-level allowlists per role. Filter the
tools/listresponse per authenticated principal so admin tools never appear to non-admin clients—and enforce the check inside the handler too, since registries can be bypassed by direct JSON-RPC. - Log and monitor tool calls. Record method, arguments, session identity, and result status. A user enumerating invoice IDs produces a loud, detectable pattern—only if you’re logging the arguments.
- Rate-limit and validate arguments. Treat
inputSchemaas input validation guidance, not authorization. Sequential ID access is enumeration regardless of schema conformance.
Blue-Team Checklist and CTF Practice Ideas
- ☐ Every tool argument that references an object has a server-side ownership check.
- ☐ No tool accepts a user/session/tenant identifier as input.
- ☐
tools/listoutput is scoped per role. - ☐ All
tools/callrequests are logged with arguments and session identity. - ☐ Autorize or equivalent runs against MCP endpoints in your CI pipeline.
To extend the lab: add JWT-based sessions, a third “admin” role, and indirect prompt injection (hide user_id: bob in an invoice memo and watch the agent forward it). Then verify your fixes hold when the attack comes through the model rather than the wire.
Frequently Asked Questions
Is BOLA the same as IDOR?
No. BOLA is the API-level vulnerability category (OWASP API1:2023): the server fails to check object ownership. IDOR is the most common manifestation—exploiting predictable object references to access others’ data. Every IDOR is a BOLA finding; not every BOLA is IDOR (object references can be GUIDs and still unprotected).
Can I legally test MCP servers for BOLA?
Only your own lab or systems with explicit written authorization. Building the local vulnerable server above is the sanctioned path—same tooling, same payloads, zero legal exposure.
Do MCP clients like Claude Desktop send tool calls through Burp?
Yes, when connected to an HTTP-based MCP transport. Set HTTP_PROXY/HTTPS_PROXY to your Burp listener before launching the client and install Burp’s CA certificate for TLS endpoints. The JSON-RPC traffic then appears in Proxy history for Repeater and Intruder work.
How do I prevent BOLA in my MCP tools?
Derive user identity exclusively from the server-side session—never from tool arguments or model output—and enforce object-level and function-level authorization checks inside every handler. Deny by default; treat the LLM client as untrusted input.
Does the LLM increase BOLA risk?
Indirectly, yes. Models accept and repeat identifiers from prompts, retrieved documents, and injected content without judgment, so missing server-side authz gets exploited through normal agent use—an attacker never needs direct API access. The agent amplifies the blast radius of an authorization gap you should have fixed anyway.
Related reading
- Anatomy of a Modern SOC: Alert Triage, Tiering and the Metrics That Expose Real Gaps
- Write Detections That Survive: From Atomic Red Team TTPs to Tuned Sigma Rules in a Home Lab




