Server Implementation
A tool accepts an identifier (document ID, ticket ID, user ID) and returns the record without verifying that the caller is allowed to see it.
IDOR is the bug where the server assumes the client will only ask for objects it is allowed to ask for. It is less about the missing check and more about the missing *ownership model* — the tool treats the ID as a fact, not as a claim to authorize.
MCP tools often wrap existing APIs that already had authorization baked in at the HTTP layer. When you wrap those APIs into a tool the LLM can drive, the authorization gets re-anchored on whatever credentials the server was given at startup — typically a single service account with broad access. The model can now ask for any object the service account could see, including objects the current end-user should not.
@server.tool() |
def get_invoice(invoice_id: str) -> dict: |
return db.fetch_one("SELECT * FROM invoices WHERE id = %s", invoice_id) |
@server.tool() |
def get_invoice(invoice_id: str, ctx: Context) -> dict: |
user_id = ctx.principal.user_id # set from the MCP auth layer |
row = db.fetch_one( |
"SELECT * FROM invoices WHERE id = %s AND owner_id = %s", |
invoice_id, |
user_id, |
) |
if not row: |
raise PermissionError("invoice not found or not yours") |
return row |
We flag database reads that use a tool argument as the primary-key filter without joining to a principal-derived column. This is a heuristic rule — false positives are common, so we surface it as HIGH evidence rather than a certain finding.
See the full threat catalog for every documented detection.
MCPSafe runs this check — and every other rule in the catalog — on any MCP server you paste in.
Scan now