Summary
At fuyuxiang/echo-agent commit 5028865ba846 (package version 0.3.2 in the
checked-out pyproject.toml), the A2A server exposes a real POST /a2a
JSON-RPC route while storing tasks in a process-global
dict[str, A2ATask] keyed only by task id.
tasks/send accepts a caller-supplied id. tasks/get and tasks/cancel
resolve the shared task map by bare task id without a caller, owner, tenant, or
session guard.
The authority path is:
Alice tasks/send(id = task-alice-private)
-> process-global A2AProtocol._tasks["task-alice-private"]
Bob tasks/get("task-alice-private")
-> Alice's in-flight task is returned
Bob tasks/cancel("task-alice-private")
-> Alice's in-flight task is canceled
Affected Target Verified
- Repository:
https://github.com/fuyuxiang/echo-agent
- Commit verified:
5028865ba846
- Package version in checkout:
0.3.2
- Components:
echo_agent/a2a/protocol.py
echo_agent/a2a/server.py
- Evidence level: source-anchored target-package replay against the checked-out
A2A protocol implementation
Preconditions
- A deployment exposes the
/a2a JSON-RPC route.
- The route is shared across more than one caller, user, or upstream agent
peer.
- An attacker can learn, guess, choose, or collide with a task id. The target
accepts caller-supplied task ids for tasks/send, and the task id is also
returned in normal A2A responses.
Steps To Reproduce
From the extracted package directory:
cd attachments
bash reproduce.sh
Expected vulnerable result:
{
"verdict": "fail-under-shared-process-a2a-task-authority-policy",
"bob_get_returned_alice_task": true,
"bob_get_leaked_secret": true,
"bob_cancel_succeeded": true
}
The replay executes the real checked-out A2A protocol code:
- Alice sends
tasks/send(id = task-alice-private) with a secret-bearing user
message.
- The target inserts Alice's task into the shared
_tasks map and leaves it
in working state while the modeled processor is blocked.
- Bob calls
tasks/get(task-alice-private) and receives Alice's task content.
- Bob calls
tasks/cancel(task-alice-private) and the target returns the same
task in canceled state.
Observed oracle facts include:
{
"alice_task_id": "task-alice-private",
"bob_get_returned_alice_task": true,
"bob_get_leaked_secret": true,
"bob_cancel_returned_alice_task": true,
"bob_cancel_succeeded": true,
"caller_supplied_task_id_preserved": true
}
Technical Details
Source facts validated by the oracle:
- the app exposes a real
/a2a route;
- A2A authentication is optional (
auth_fn: AuthFn | None = None);
- tasks are stored in
self._tasks: dict[str, A2ATask] = {};
tasks/send accepts a caller-supplied params["id"];
tasks/get returns self._tasks.get(task_id) by bare id;
tasks/cancel mutates self._tasks.get(task_id) by bare id;
- the same task id is also promoted into the downstream
session_key=f"a2a:{task.id}".
Expected Behavior
Task-control operations should bind the loaded task to the authenticated
principal, tenant, or an equivalent caller authority before returning or
mutating it. taskId should identify the object, not authorize access to it.
Actual Behavior
The A2A task-control methods treat bare task id as sufficient authority. A
second caller that knows another caller's task id can read the in-flight task
and cancel it.
Impact
Depending on deployment and task contents, this can cause:
- unauthorized task history or prompt disclosure through
tasks/get;
- unauthorized task lifecycle mutation through
tasks/cancel;
- cross-caller task-id collisions because the target accepts caller-supplied
ids for new tasks.
There is also an adjacent session-binding concern: the same task.id becomes
the downstream agent-loop session key. This report does not rely on that second
step for the core claim, but it increases the importance of fixing task-id-only
authority at the A2A boundary.
Suggested Fix
- bind every task read and mutation path to authenticated caller authority;
- if the target supports multi-user or multi-peer deployments, persist task
ownership metadata and check it before tasks/get and tasks/cancel;
- do not treat caller-supplied task ids as globally authoritative without a
per-caller namespace or ownership check;
- add Alice/Bob regression tests for
tasks/get and tasks/cancel;
- review whether downstream
session_key=f"a2a:{task.id}" should also bind
principal or tenant information.
Attachments
attachments.zip
Summary
At
fuyuxiang/echo-agentcommit5028865ba846(package version0.3.2in thechecked-out
pyproject.toml), the A2A server exposes a realPOST /a2aJSON-RPC route while storing tasks in a process-global
dict[str, A2ATask]keyed only by task id.tasks/sendaccepts a caller-supplied id.tasks/getandtasks/cancelresolve the shared task map by bare task id without a caller, owner, tenant, or
session guard.
The authority path is:
Affected Target Verified
https://github.com/fuyuxiang/echo-agent5028865ba8460.3.2echo_agent/a2a/protocol.pyecho_agent/a2a/server.pyA2A protocol implementation
Preconditions
/a2aJSON-RPC route.peer.
accepts caller-supplied task ids for
tasks/send, and the task id is alsoreturned in normal A2A responses.
Steps To Reproduce
From the extracted package directory:
cd attachments bash reproduce.shExpected vulnerable result:
{ "verdict": "fail-under-shared-process-a2a-task-authority-policy", "bob_get_returned_alice_task": true, "bob_get_leaked_secret": true, "bob_cancel_succeeded": true }The replay executes the real checked-out A2A protocol code:
tasks/send(id = task-alice-private)with a secret-bearing usermessage.
_tasksmap and leaves itin
workingstate while the modeled processor is blocked.tasks/get(task-alice-private)and receives Alice's task content.tasks/cancel(task-alice-private)and the target returns the sametask in
canceledstate.Observed oracle facts include:
{ "alice_task_id": "task-alice-private", "bob_get_returned_alice_task": true, "bob_get_leaked_secret": true, "bob_cancel_returned_alice_task": true, "bob_cancel_succeeded": true, "caller_supplied_task_id_preserved": true }Technical Details
Source facts validated by the oracle:
/a2aroute;auth_fn: AuthFn | None = None);self._tasks: dict[str, A2ATask] = {};tasks/sendaccepts a caller-suppliedparams["id"];tasks/getreturnsself._tasks.get(task_id)by bare id;tasks/cancelmutatesself._tasks.get(task_id)by bare id;session_key=f"a2a:{task.id}".Expected Behavior
Task-control operations should bind the loaded task to the authenticated
principal, tenant, or an equivalent caller authority before returning or
mutating it.
taskIdshould identify the object, not authorize access to it.Actual Behavior
The A2A task-control methods treat bare task id as sufficient authority. A
second caller that knows another caller's task id can read the in-flight task
and cancel it.
Impact
Depending on deployment and task contents, this can cause:
tasks/get;tasks/cancel;ids for new tasks.
There is also an adjacent session-binding concern: the same
task.idbecomesthe downstream agent-loop session key. This report does not rely on that second
step for the core claim, but it increases the importance of fixing task-id-only
authority at the A2A boundary.
Suggested Fix
ownership metadata and check it before
tasks/getandtasks/cancel;per-caller namespace or ownership check;
tasks/getandtasks/cancel;session_key=f"a2a:{task.id}"should also bindprincipal or tenant information.
Attachments
attachments.zip