A $20 Approval Ran as $2,000 in A2A Test

A field note shows an A2A Task approval for a $20 transfer was spent on a $2,000 transfer after a same-ID update on LangGraph Agent Server.

A $20 Approval Ran as $2,000 in A2A Test

Adithyan Arun Kumar demonstrated a flaw where a single human approval can be redirected to authorize a completely different action. His Loopjacking test targeted an A2A implementation built on LangGraph Agent Server.

In the controlled scenario, the approval role read a human-in-the-loop interrupt for mock_wire_transfer(20, approved-vendor) and later submitted approve for that view. Between view and decision, a separate maker sent another message through the server’s A2A route that replaced the pending call with mock_wire_transfer(2000, attacker-sink). The mock ledger then recorded 2,000 units moved under the approver’s authority.

The Task ID stayed the same.

That is the core confusion Kumar isolates. An A2A Task answers which work a message belongs to, not which exact tool call a human authorized. The specification before section 7.6.4 treated TASK_STATE_AUTH_REQUIRED as a nonterminal pause where clients could send messages to negotiate or correct a request, and where a credential could arrive out of band. It left open who defines the approval scope, what canonical operation was shown, and what must be checked before use. An implementation that checks only “this Task was approved” can spend decision D_A on operation B. An implementation that compares the current executable operation to the one bound to D_A rejects B or asks again.

The timeline makes the gap visible. Issue #2080 raised the ambiguity. PR #2081, merged into main, added section 7.6.4. It states TASK_STATE_AUTH_REQUIRED signals a need for authorization, not a grant, that scope is defined by the implementation, credential issuer or extension, and that later Task messages are not implicitly covered. When checked , the A2A releases page still listed v1.0.1 as the latest tag, so a tagged release should not be assumed to include the clarification.

The product result Kumar tested is narrower and concrete. He exercised Agent Server’s shipped A2A JSON-RPC message/send endpoint and its message.command.update surface alongside the official human-in-the-loop middleware, using LangChain 1.3.18, LangGraph 1.2.11, and an in-memory server with synthetic identities and a deterministic local model. The maker’s update named the existing taskId and contextId and carried command.update.messages[0].id = approval-action and tool_calls[0].id = transfer-call with args B. The IDs kept the replacement pinned to the pending work, while the arguments changed from (20, approved-vendor) to (2000, attacker-sink). Server code forwarded the command, LangGraph’s reducer replaced the pending same-ID call, and the approver’s later approve reconstructed the call from current thread state and executed B.

Controls matter here more than status codes. The experiment checked the view before mutation, Auth decisions, state after mutation, and an append-only mock ledger. The attack thread’s ledger recorded B and no A. A separate control where A stayed unchanged recorded A. A maker-only attempt without the approver did not reach the ledger. A strict positive also denied maker resume and outsider access. When the tested Auth policy denied maker updates while approval was pending, B was blocked and A still executed.

Beyond this harness, the pattern has drawn wider attention. The Agentic Security Newsletter noted the same class has been reproduced in Agno AgentOS as well as LangGraph, which extends the conversation beyond one server composition.

Version evidence is scoped, not universal. The strict trace ran on 12 sampled released Agent Server versions from 0.7.5 through 0.14.0, with 0.7.4 as a feature-absent control that did not consume the needed command-update path. A separate hash-verified scan found the forwarding path in source for 130 releases from 0.7.5 through 0.13.4, but those are source evidence, not 130 runtime demonstrations. The production Postgres image was not tested because it reached its license-key requirement before the client test ran, and the archive identifies no vendor-fixed LangGraph release.

The durable fix is not a Task ID check. It is binding the decision to the exact effect and checking it at use time, or denying the state mutation that makes the swap possible when workflow allows it. Kumar’s safe-control excerpt denies maker command.update inside threads/create_run, which stopped pending-state substitution in this composition but does not cover cases where authorized updates or retries legitimately change pending work.

© 2026 StartupHub.ai. All rights reserved. You may not republish this article in full without a license. Search engines and AI research tools may crawl and summarize for reference. Bulk reproduction or model training requires a license. See our terms.
Daniel Singer

Written by

Daniel Singer

Editor, StartupHub.ai

Daniel Singer is the editor of StartupHub.ai, a technology expert and thought leader on AI and its applications across sectors, from fintech and healthcare to developer tooling and consumer software. He writes and tests the tools covered here thoroughly and regularly, and built StartupHub.ai to give founders, operators and buyers a clearer read on what they are actually being sold.