skip to content

Why must an MCP server never treat possession of a state handle as authentication?

level: seniorimportance: should knowfreq 42%

answer

  1. it is a name, not a key
  2. the model has already seen it
  3. transcripts and logs are public surfaces
  4. identity comes from the token instead
  5. the old session-hijacking entry, renamed

basics

~20 s

Because a handle is a reference, not a credential. MCP 2026-07-28 names this threat State Handle Hijacking and states that possession MUST NOT be treated as authentication: handles pass through model context, transcripts and logs, so anyone who sees one could otherwise act as the original caller.

solid answer

~50 s

When revision 2026-07-28 removed protocol sessions, the threat model's "Session Hijacking" entry was replaced by **State Handle Hijacking**, and its rule is blunt: possession of a state handle MUST NOT be treated as authentication. The reason is that these handles are ordinary tool arguments — they travel through the model's context window, through the host's transcript, through logs and traces, and a model can echo one into a different tool or a different server. Treating a handle as a bearer credential would make every one of those surfaces an account takeover. The correct design binds the handle to the authenticated principal when it is minted and re-checks that binding on every use against the credential presented on the *current* request — for a remote server, the audience-bound OAuth 2.1 access token, not `io.modelcontextprotocol/clientInfo`, which is self-reported and untrusted. Handles should also be unguessable and short-lived, and an unknown or foreign handle should be handled like any unauthorized object reference.

go deeper

for a junior

Remember the rule itself: a state handle says which object a call is about, never who is calling. Identity comes from the request's credential, checked every time.

for a middle

Explain why the exposure is worse than a session header: handles ride in tool arguments through model context, transcripts and logs, so any of those becoming visible would grant access if possession implied identity.

for a senior

Describe the mechanic end to end — bind the handle to the validated token's principal at mint, re-check ownership on every use, respond identically to unknown and foreign handles, keep values opaque, unguessable and short-lived.

for a principal

Own the boundary between the authorization layer and the state layer across a fleet: tokens establish identity and scope, handles name objects, step-up is a token concern, and no server-side design may blur the two for convenience.

## Where the threat comes from Revision 2026-07-28 removed protocol sessions, and with them the old "Session Hijacking" concern about a stolen `Mcp-Session-Id`. But the need for continuity across calls did not disappear; it moved into server-minted handles that the client passes back as ordinary tool arguments. So the threat moved with it, and the security material was renamed accordingly to **State Handle Hijacking**, carrying one hard rule: possession of a state handle MUST NOT be treated as authentication. ## Why a handle leaks more readily than a session id A session id lived in a header. Headers are seen by the transport layer and, at worst, by proxies and access logs. A state handle lives somewhere much more exposed: - **The model's context.** The handle came back inside a tool result, so the model read it, and it will be re-emitted in the arguments of the next call. Anything in context is in the transcript. - **The host's UI and history.** Users can often inspect tool calls; transcripts get exported, pasted into bug reports, and stored. - **Logs and traces.** Tool arguments are the first thing an operator logs when debugging a tool. - **Other servers.** A host may have several MCP servers connected. A model that has a `workspaceId` in context can put it in an argument to a completely different server's tool, deliberately or through a prompt-injected instruction in some content it read. If the handle were sufficient to act, each of those is a path to acting as somebody else. ## What correct handling looks like **Bind at mint time.** When the server creates the handle, record the authenticated principal it belongs to — the subject of the validated access token, plus the tenant or organisation scope. The handle is a pointer to state *owned by* someone. **Authorize on every use.** Each incoming request carries its own credential, because every request in 2026-07-28 is self-contained. Validate that credential, then check that the principal it identifies is the owner of the handle presented. A handle that belongs to another principal is an unauthorized object reference and should be treated as one — same response as a handle that does not exist, so you do not confirm its existence to a prober. **Do not authenticate from anything self-reported.** `io.modelcontextprotocol/clientInfo` on a request and `io.modelcontextprotocol/serverInfo` on a result are explicitly untrusted, self-reported identity. Neither is an authorization input. Similarly, `ToolAnnotations` such as `readOnlyHint` and `destructiveHint` are hints clients MUST treat as untrusted unless the server is trusted; they are not server-side access control either. **Make handles unguessable and short-lived.** Use enough entropy that enumeration is hopeless, and set a TTL so a leaked transcript has a limited window. Give the client a clean, actionable error when a handle has expired, so the model re-mints rather than looping. **Carry no secrets inside.** Assume the handle string is public the moment it is issued. Never encode credentials, tokens, personal data or internal identifiers you would not publish. **Scope narrowly.** A handle should authorize the smallest thing that makes the workflow work — one workspace, one paginated query, one upload — never "this user's whole account". Narrow scope caps the damage when one does leak. ## The relationship to OAuth For remote servers, revision 2026-07-28 keeps the MCP server in the role of an OAuth 2.1 **resource server**: it validates audience-bound access tokens and MUST NOT pass tokens through to upstream services. The handle sits strictly below that layer — it selects *which object* the request is about, while the token establishes *who is asking*. Keeping the two roles separate is the whole answer to this question. If a step-up in privilege is needed, that is expressed at the token layer, with `403` and `error="insufficient_scope"`, not by minting a more powerful handle. ## A useful comparison A capability-style token — a signed, self-describing object that *is* the authority — is a legitimate design in other systems, but it is not what an MCP state handle is, and adopting that model here would collide directly with the spec's rule. If you genuinely want capability semantics, the authority still has to be derived from the validated request credential; the handle stays a name for state. ## Answering well State the rule, explain *why* the exposure profile makes it necessary (model context and transcripts, not just headers), then give the concrete mechanic: bind on mint, re-check ownership against the request's own credential on every use, keep handles opaque, unguessable and short-lived.

  • How should the server respond when a request presents a handle owned by a different principal?
    The same way it responds to a handle that does not exist. Distinguishing the two confirms existence to an attacker probing for valid identifiers. Log the attempt with the authenticated principal for detection, return a generic not-found or invalid-argument tool error, and do not include any detail about the real owner or contents. Treat it as an unauthorized object reference, not as a lifecycle problem.
  • Is clientInfo useful for tying a handle to the caller that minted it?
    No. `io.modelcontextprotocol/clientInfo` in `_meta` is self-reported and explicitly untrusted — a caller can put anything there. It is fine for telemetry and for shaping messages, never for authorization. Bind handles to the subject of a validated, audience-bound access token instead, since that is the only identity the server has actually verified on the current request.
  • What is the risk if a model passes one server's handle into another server's tool?
    With correct design, nothing useful happens: the second server never minted it and does not recognise it, and the first would reject it because the presenting principal must own it. The risk appears when a server accepts handles on possession alone, or when the handle itself embeds sensitive data — then simply appearing in context is enough to leak it, since MCP gives servers no way to see into one another and no way to recall a value the model has read.
  • Does a short TTL substitute for authorization checks?
    No. It only narrows the window. Within that window a leaked handle is fully usable if possession is treated as authority, and workflows deliberately keep handles alive for as long as the work takes. TTL is defence in depth alongside ownership checks, unguessable values and narrow scope — never in place of them.

A handle is a table number in a restaurant, not a signed cheque. Knowing the number tells the waiter which table you mean; it should never be what proves the bill is yours to charge.

saying these in an interview costs you the question

  • Accepts a handle as proof of who is calling
  • Trusts clientInfo in _meta to identify the caller
  • Encodes tokens or personal data inside the handle
  • Checks ownership only when the handle is first minted
  • Uses short, sequential handle values that can be enumerated

context