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.
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.