Topic: mcp server idor security
MCP server IDOR security — insecure direct object reference in tool parameters
Insecure direct object reference (IDOR) is one of the most common authorization vulnerabilities in web APIs, and it translates directly to MCP servers that expose tools accepting user IDs, file IDs, document IDs, or resource keys as parameters. When an MCP tool accepts an identifier from the LLM and looks up the resource directly without verifying that the authenticated caller owns or has permission to access that resource, any caller — or any injection that controls the LLM's tool arguments — can enumerate or access arbitrary resources by iterating IDs.
The IDOR pattern in MCP tools
Consider a document management MCP server with a get_document tool. The tool accepts a document_id and returns the document content. The intent is that users access only their own documents. But the tool looks up the document directly without binding the lookup to the authenticated user's context:
// VULNERABLE — looks up document_id without checking ownership
const getDocument = server.tool('get_document', {
document_id: z.string().uuid().describe('Document ID to retrieve'),
}, async ({ document_id }) => {
// No check: does the authenticated user own this document?
const doc = await db.get('SELECT * FROM documents WHERE id = ?', [document_id]);
if (!doc) throw new Error('Document not found');
return { content: [{ type: 'text', text: doc.content }] };
});
The LLM calling this tool can pass any UUID as document_id. If document IDs are UUIDs, they're not directly guessable — but an injected instruction can instruct the LLM to call get_document with a specific known ID (e.g., from a leaked reference in another document or a known pattern). If document IDs are sequential integers formatted as UUIDs or any predictable scheme, the attacker can enumerate the entire document store.
The IDOR risk is amplified in MCP servers because the LLM processes arbitrary content. A document that contains text like "Document ID 550e8400-e29b-41d4-a716-446655440000 contains the Q3 financial projections" followed by an injected instruction can cause the LLM to call get_document with that ID — a cross-document data exfiltration attack. No vulnerability in the LLM or the MCP transport is required; the tool simply does what it was asked.
IDOR in multi-tenant MCP servers
The IDOR risk is highest in multi-tenant MCP servers where users share the same server instance. A multi-tenant deployment where each user authenticates via a bearer token and each tool call carries the user's identity has the right infrastructure — but if any tool looks up a resource by a caller-supplied ID without a WHERE clause that also filters by the authenticated user's ID, the access control is absent where it matters most.
// STILL VULNERABLE — filters by org_id from token but not by user_id
async function getDocument(ctx, { document_id }) {
const { user_id, org_id } = ctx.auth; // from verified JWT
// Filters to org, but all org members can read all org documents
// regardless of per-document ACL
const doc = await db.get(
'SELECT * FROM documents WHERE id = ? AND org_id = ?',
[document_id, org_id]
);
return doc;
}
Filtering by org prevents cross-org access but doesn't enforce per-document ACLs. Whether that's a bug depends on your access model — but it's a common source of unintended access when the intent was "only the document creator can read their own documents."
The correct pattern: bind lookups to the caller's identity
The safe pattern binds every resource lookup to the authenticated caller's identity at the SQL query level, not at the application layer:
// SAFE — document lookup requires both correct ID AND ownership
async function getDocument(ctx, { document_id }) {
const { user_id } = ctx.auth;
const doc = await db.get(
`SELECT d.* FROM documents d
JOIN document_acl acl ON acl.document_id = d.id
WHERE d.id = ? AND acl.user_id = ? AND acl.can_read = 1`,
[document_id, user_id]
);
if (!doc) {
// Return the same error whether not found or not authorized —
// don't reveal existence of documents the caller can't access
throw new Error('Document not found or access denied');
}
return doc;
}
Three important details: First, the JOIN enforces the ACL at the database level, not in application code after the lookup. Second, the error message is the same for "not found" and "unauthorized" — this prevents oracle attacks where an attacker infers document existence from the error type. Third, the query returns nothing (not just omitting the content) for unauthorized documents, so there's no race between the authorization check and the return.
How SkillAudit detects IDOR
SkillAudit's LLM-probe axis sends tool calls with known-invalid IDs and IDs constructed to match common sequential patterns. If the tool returns different errors for "ID not found" vs. "ID found but access denied," that differential response is flagged as an authorization oracle finding. If the static analysis finds database queries where a user-supplied ID appears in a WHERE clause without a corresponding caller-identity filter, that's flagged as a potential IDOR finding for manual review. See the access control and multi-tenant security pages for related patterns.
Check your MCP server's authorization logic for IDOR vulnerabilities.
Run a free audit →