Skip to content

[BUG] echo-agent A2A Global Task Authority #12

Description

@N0zoM1z0

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:

  1. Alice sends tasks/send(id = task-alice-private) with a secret-bearing user
    message.
  2. The target inserts Alice's task into the shared _tasks map and leaves it
    in working state while the modeled processor is blocked.
  3. Bob calls tasks/get(task-alice-private) and receives Alice's task content.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions