Skip to content

The Downgrade Attack Hiding Inside Your A2A and MCP Handshake

9 min read Fullmakt Team

  • a2a
  • mcp
  • authentication
  • agents
  • governance
  • traceability

Every TLS-literate engineer knows the shape of a downgrade attack: offer a weaker version of the protocol during the handshake, hope the other side accepts it, and strip out whatever security improvements the newer version added. TLS spent two decades hardening its handshake against exactly this — version-intolerance checks, TLS_FALLBACK_SCV, signed transcripts that make a tampered negotiation detectable. The A2A initialize/agent-card negotiation and the MCP initialize handshake do the same kind of version negotiation, in the same place in the connection lifecycle, with none of that hardening — because both specs treat protocol-version mismatch as a compatibility problem to resolve gracefully, not as a security decision an attacker might have a reason to force.

The failure: negotiation without a floor

An MCP session starts with the client sending initialize and a protocolVersion string; the server replies with the version it supports. If the two don’t match, the spec’s guidance is polite accommodation — fall back to whatever the older side understands — not refusal. An A2A client resolves capabilities the same way, reading whatever the target’s agent card advertises and adapting to it. Nothing in either handshake asks “is this the lowest version either of us is willing to accept,” because neither handshake was designed with an adversary who controls one side of the negotiation in mind. It was designed for the ordinary case: a client and server built months apart, doing their best to still talk to each other.

That’s a reasonable default for compatibility. It’s a bad default for authentication, because a version string isn’t just a feature flag — in both specs it’s also where security-relevant capability gets attached. A newer MCP protocolVersion can imply support for stricter auth flows or capability negotiation the older one never had. A newer A2A capability set can imply the signing, encryption, or push-notification authentication this series has already written about. Whoever controls what version gets offered controls which of those protections actually apply to the session — and in a handshake with no floor, the party best positioned to control that offer is whichever side is compromised, malicious, or simply running an old build nobody patched.

Why this is a login-handshake problem, not a compatibility problem

A login handshake’s entire job is to let each side prove something about itself before anything sensitive moves. A version-negotiation step that silently falls back is a step in that same handshake that a third party can influence without proving anything at all. Consider what a man-in-the-middle — or a compromised intermediary sitting between two agents in a delegation chain — actually needs to do: it doesn’t have to forge a signature or steal a token. It only has to intercept the initialize exchange and rewrite the offered version downward, to whichever version has the weakest capability set either side still accepts. Every downstream check in that session then runs against the weaker contract, and nothing in the transcript records that a stronger one was available and skipped.

This is the same category of failure this series keeps returning to under different names — MCP rug pulls swap what a tool does after approval; agent card spoofing swaps who an agent thinks it’s talking to. A protocol downgrade swaps how strong the proof was allowed to be, at the one moment in the session — the very first round trip — when the strongest proof should be non-negotiable. It doesn’t need a payload, a forged identity, or a stolen credential. It just needs a position in the network and a spec that says “adapt” where it should say “refuse.”

Why this counts as agentic AI gone wrong

The failure mode isn’t hypothetical inconvenience — it’s the standard AI-agents-gone-wrong shape: an agent completes its task, the logs show a successful handshake and a successful call, and nobody notices that the handshake succeeded on a weaker version than either party actually shipped. Incident review after the fact has no reason to look at protocol-version fields, because protocolVersion: "2024-11-05" doesn’t read as an anomaly — it reads as a version string, indistinguishable from an old client that legitimately hasn’t updated. The gap between “this session negotiated down” and “this session was attacked” is invisible to anyone who isn’t specifically logging and diffing the offered version against the ceiling both sides are capable of.

Where the fix actually has to live

  • Pin the floor, don’t negotiate it. A server that supports a stronger protocol version should refuse to complete the handshake at a weaker one rather than accommodate it — the same principle TLS eventually adopted after enough downgrade attacks made “graceful fallback” indefensible as a default.
  • Log the negotiated version on every session, not just the outcome. An audit trail that shows “handshake succeeded” without the version it succeeded at can’t answer the one question that matters after an incident: was this session ever offered the weaker contract, or did it start there. That’s the same completeness gap this series has flagged in audit logs that record actions but not the identity chain behind them.
  • Treat capability negotiation as a security decision, not a compatibility shim. If a version bump adds signing, stricter scopes, or mandatory push-notification authentication, falling back to the older version should cost the session something visible — a lower trust tier, a mandatory approval step — not a silent, capability-equivalent handshake.
  • Don’t let the client decide the floor. In both A2A and MCP, the client typically proposes the version. A negotiation where the offering side also sets the acceptable minimum is a negotiation with no adversarial model at all; the receiving side has to hold its own floor independently.
  • Assume a downgraded session is a lower-privilege session, structurally. The fix isn’t just detection — a broker sitting on the connection should scope what a downgraded handshake is allowed to reach, the same way this series has argued a DPoP-less bearer token should be trusted with less than a sender-constrained one, because the handshake itself already told you it proved less.

The business case: a fixed floor, not a negotiated one

Fullmakt’s own MCP and A2A endpoints don’t negotiate a version down. The remote MCP surface (src/Fullmakt.Api/Endpoints/RemoteMcpEndpoints.cs) responds to every initialize call with a single, fixed protocolVersion constant regardless of what the client offered; the A2A surface does the same with its own pinned protocol version. There’s no code path where a client can talk Fullmakt’s broker down to an older, weaker contract — the version a session gets is the version the broker ships, not the version an attacker in the middle would prefer it accept.

That fixed-floor posture is the same discipline this series has described at every other layer of the broker:

  • Every credential Fullmakt dispenses is scoped and short-lived regardless of which protocol version a downstream call arrived on — a downgraded handshake to a third-party MCP server doesn’t widen what the credential behind that connection is allowed to reach, because the scoping happens at dispense time, not as a property of the negotiated session.
  • Every brokered call is logged with the connection it went through, so a security review asking “did any of this agent’s sessions negotiate down” has an answer in the audit trail, not a gap where the version field should have been.
  • Policy evaluation doesn’t inherit trust from a handshake it didn’t perform. Whatever version a third-party A2A or MCP peer negotiated with an agent, Fullmakt’s policy engine still evaluates the call on its own terms — the credential handoff is the chokepoint, not the peer’s handshake outcome.

The commercial logic is the same one behind every other control in this series: a login handshake that can be talked down is a login handshake that was never really enforcing the floor it claimed to. Anchoring enforcement at the credential broker — which doesn’t take the negotiated protocol version’s word for what a session is allowed to do — means a downgrade attack against a third-party peer costs an agent a weaker conversation, not a wider blast radius.

FAQ

Do A2A and MCP actually support protocol-version negotiation today? Yes — both specs include an initialize-time (or agent-card-time) exchange where each side states a supported version, and both specs describe graceful fallback rather than mandatory refusal when versions differ.

Is a downgrade attack here the same thing as an MCP rug pull? Related but distinct. A rug pull changes what a specific, already-approved tool does. A protocol downgrade changes the security properties of the entire session before any tool is even listed — it’s an attack on the handshake itself, not on a tool definition negotiated after it.

Who would actually be positioned to force a downgrade? Anyone sitting between the two parties in the connection — a compromised network intermediary, a malicious or compromised hop in an A2A delegation chain, or a misconfigured proxy — since neither handshake requires the negotiated version to be cryptographically bound to anything that would reveal tampering.

Does pinning a fixed protocol version break compatibility with older clients? It trades graceful degradation for a hard failure on mismatch, which is the same tradeoff TLS made once downgrade attacks stopped being theoretical. For a broker mediating credentialed access, a client that can’t complete the handshake at the required version failing loudly is preferable to it succeeding quietly at a weaker one.

Does Fullmakt log the protocol version negotiated on every session? Fullmakt doesn’t negotiate a version down in the first place — its MCP and A2A endpoints respond with a fixed protocol version — so there’s no downgraded state for a session to be in on Fullmakt’s own surface. Calls Fullmakt brokers to third-party peers are still logged and scoped at the credential layer independent of whatever version that peer’s handshake settled on.

A TLS handshake that can be talked down to SSLv3 isn’t secure just because it’s still called TLS. An A2A or MCP handshake that can be talked down to an older protocolVersion isn’t secure just because it’s still called authentication — and until both specs treat a version floor as mandatory rather than advisory, that gap is a governance program’s to close, at whichever layer still verifies what a session actually proved.