Security

Faking a Yes: 6 Attacks on the Human Confirmation in a .NET 10 MCP Server

A developer red-teamed the confirmation step of a refund MCP server built on ASP.NET Core with the official C# SDK (ModelContextProtocol 2.3.0). Six attacks — plain-JSON requestState, replaying another user's approval, injected form fields and a pre-checked consent box — each led to a concrete server-side defense, plus a checklist you can apply to your own tools.

DEV Community: #dotnet MICHAEL-MAURICE
Published
Reading time
10 min read
agent run #18
agent run #18
Go to original dev.to

Report this story

Is something wrong with the summary, or do you own this content? Your message comes straight to my inbox.

Type of message

Key points

  • Never ship requestState as plain JSON or base64; seal it and bind it to the tool, an arguments digest, the user and an expiry
  • Reject inputResponses that arrive without state, otherwise nothing proves which question the answers belong to
  • The SDK's IsAccepted is case-insensitive, so "ACCEPT" passes — never use it as the consent check
  • Consent fields must be required and default-free, because the SDK client fills defaults into an accepted response
  • Rebuild the form from current data on return and compare fingerprints; if it changed, ask the user again
  • Route secrets such as an IBAN through URL mode with a same-user check instead of a form field
  • Retries landing on other instances need the same Data Protection key ring and application name everywhere

In MCP a tool on a stateless server cannot wait for an answer: it ends the call with input_required, a form to render and an opaque requestState, and the client re-invokes the same tool with the answers in inputResponses. The author's refund desk uses that flow from the 2026-07-28 spec so the "yes" comes from a person, not the model — and the attacks target exactly those client-supplied halves.

The fixes are concrete: requestState is sealed with ASP.NET Core Data Protection and bound to the tool, a SHA-256 digest of the canonical arguments, the user and an expiry, so swapping the amount or replaying another user's approval fails. IsAccepted is case-insensitive and never treated as consent; form and validator share one FormSpec, consent fields are required with no default, and the form is rebuilt and fingerprint-compared on return. Secrets go through URL mode, and single-use approval stays open.

Real-world use

  • Building an MCP tool that confirms irreversible actions such as refunds, account closure or resource deletion
  • Carrying approval state safely in a stateless, multi-instance ASP.NET Core MCP server
  • Collecting sensitive data like a payout IBAN without leaking it into the conversation, using URL-mode elicitation
  • Setting up a red-team checklist and startup form lint rules for your own confirmation tools

Why it matters

If the "yes" can be forged, bent or pre-filled, the confirmation step becomes the attack surface itself — and the mistakes here move real money or delete real data.

This summary was generated by the agent from the original post on DEV Community: #dotnet. The full text and all rights belong to its author and publisher. Go to original

Related stories

Feedback

Found a bug or have an idea? Your message lands straight in my inbox.

Type of message