혼란스러운 대리인 또는 “토큰을 전달하기만 하면” MCP 서버가 중단되는 이유

작성자

카테고리:

← 피드로
DEV Community · Yimmie Honrodt · 2026-09-06 개발(SW)

Yimmie Honrodt

Here is a failure that does not look like a bug in code review, passes every test you wrote, and quietly hands an attacker your privileges. It is called the confused deputy, and it is the reason “just forward the token” is one of the most expensive shortcuts you can take in an MCP server.

The deputy, in one paragraph

A deputy is a program that acts for other people while holding more authority than they do. A confused deputy is one that can be talked into using that authority for a request it should have refused. Your MCP server is a deputy by design: it sits between an AI client and your downstream systems, and it usually holds broad credentials for those systems.

It arrives in two shapes

Shape 1: token passthrough

The client sends a token, your server forwards that same token unchanged to a downstream API.

  • The downstream system trusts your server, so the call goes through.
  • Your server never asked whether this caller was allowed to do this thing.
  • Your audit log records “the server did it”. The actual actor is invisible.

Shape 2: accepting foreign-audience tokens

Your server accepts a token whose audience was issued for a different service. Anyone who can obtain such a token, for any service you happen to accept, can now drive your server.

The MCP specification does not hedge on either shape. Servers “MUST only accept tokens that are valid for use with their own resources” and “MUST NOT accept or transit any other tokens”. If the server calls an upstream API, it acts as an OAuth client to that API, with a separate token of its own.

Why it ships by accident

Three reasons, and none of them is carelessness.

Libraries make passthrough the path of least resistance. It works in every happy-path test, because the happy-path caller genuinely is authorized. And the gap only appears with a caller who should not be authorized, which is exactly the case nobody writes a test for.

There is a variant that catches proxy authors specifically. If your server sits in front of a third-party authorization server with a static client ID, a user who consented once may never see a consent screen again, and a stolen authorization code can be redeemed on their behalf. That is why the spec requires consent per dynamically registered client rather than once per proxy.

How to find it in your own code

  1. Trace where downstream calls get their credentials. If the answer is “the incoming token”, you have shape 1.
  2. Check whether the server validates the audience against its own canonical resource URI. If it does not, you have shape 2.
  3. Prove it. Take a token minted for another audience, or another tenant, and make a tool call. It has to fail.

How to fix it

  • Validate the audience strictly. The token has to be for you.
  • Authorize per call against the actual end user and tenant, not merely “is this token valid”. There is no protocol-level session to lean on, so per-request is the only granularity there is.
  • Use your own credentials, or a token exchange, for downstream calls, so the audit trail can name the real actor.
  • Log actor, tenant, tool and outcome. A passthrough design structurally cannot produce a truthful audit log, and that is the part that hurts six months later when somebody asks what happened.

Forwarding the token buys you five minutes and sells an invisible privilege-escalation path. Treat the server as a deputy that re-checks authority on every single call, for the specific caller, and never lends its keys to a request it did not verify itself.

One thing an outside scan can tell you, and one it cannot. It can check whether your protected resource metadata declares a resource at all, because without a declared audience no client can bind a token to you and the whole defence never gets off the ground. It cannot watch what your server does with the token afterwards. That part needs the code, or somebody who reads it.

The metadata half is in my scanner: mcp-sec-scan.

원문에서 계속 ↗