Skip to content

[Bug] Routa A2A Global Task Bridge Authority #581

Description

@N0zoM1z0

Summary

At phodal/routa commit 73eb1ed1b719, the application exposes real A2A task
surfaces over both REST and JSON-RPC while storing tasks in a process-global
A2ATaskBridge.

The bridge accepts caller-selected workspaceId and contextId, persists
those values on created tasks, and can filter by them when the caller supplies
filters. But the exposed A2A routes also provide unfiltered or bare-id paths:

  • GET /api/a2a/tasks widens across workspaces when no workspace filter is
    supplied;
  • GET /api/a2a/tasks/{id} reads a task by bare id;
  • POST /api/a2a/tasks/{id} cancels a task by bare id;
  • JSON-RPC tasks/get and tasks/cancel also act by bare task id.

The authority path is:

Alice tasks/send(metadata.workspaceId = ws-alice, contextId = ctx-alice)
-> global A2ATaskBridge task entry

Bob GET /api/a2a/tasks (no workspace filter)
-> bridge.listTasks() over global bridge state
-> sees Alice task

Bob GET /api/a2a/tasks/{aliceTaskId}
-> bridge.getTask(aliceTaskId)
-> reads Alice task

Bob POST /api/a2a/tasks/{aliceTaskId}
or JSON-RPC tasks/cancel(aliceTaskId)
-> bridge.cancelTask(aliceTaskId)
-> cancels Alice task

Affected Target Verified

  • Repository: https://github.com/phodal/routa
  • Commit verified: 73eb1ed1b719
  • Components:
    • src/core/a2a/a2a-task-bridge.ts
    • src/app/api/a2a/tasks/route.ts
    • src/app/api/a2a/tasks/[id]/route.ts
    • src/app/api/a2a/rpc/route.ts
  • Evidence level: source-anchored target-package replay against the checked-out
    A2A bridge and route logic

Preconditions

  • A deployment exposes the shared /api/a2a/* REST and/or JSON-RPC routes.
  • The A2A surface is reachable across more than one workspace, caller, or
    upstream agent peer.
  • An attacker can learn another caller's task id. The target returns task ids
    in normal A2A responses and list routes.

Steps To Reproduce

From this report directory:

bash reproduce.sh

Expected vulnerable result:

{
  "verdict": "fail-under-principal-bound-a2a-task-authority-policy",
  "failing_operations": [
    "rest.tasks.list",
    "rest.tasks.get",
    "rest.tasks.cancel",
    "rpc.GetTask",
    "rpc.CancelTask"
  ]
}

The replay executes the checked-out bridge implementation and validates the
route semantics it is wired to:

  1. Alice creates a task with workspaceId=ws-alice and contextId=ctx-alice.
  2. Bob creates a separate task in ws-bob.
  3. Bob lists tasks without a workspace filter and sees Alice's task.
  4. Bob reads Alice's task by bare task id through the same bridge.
  5. Bob cancels Alice's task by bare task id through both REST and JSON-RPC
    bridge operations.

Observed oracle facts include:

{
  "verdict": "fail-under-principal-bound-a2a-task-authority-policy",
  "failing_operations": [
    "rest.tasks.list",
    "rest.tasks.get",
    "rest.tasks.cancel",
    "rpc.GetTask",
    "rpc.CancelTask"
  ],
  "middleware_files_found": false,
  "singleton_shared": true
}

Technical Details

Source facts validated by the oracle:

  • the app exposes a process-global A2A task bridge singleton;
  • createTask() preserves caller-selected workspaceId and contextId;
  • listTasks() filters by workspace only when a workspace filter is supplied;
  • REST ListTasks widens to every workspace when no filter is supplied;
  • REST GetTask and CancelTask load from the bridge by bare task id;
  • JSON-RPC GetTask and CancelTask also load from the bridge by bare task
    id.

The negative controls are important:

  • workspaceId=ws-bob filtered list does not leak Alice's task;
  • contextId=ctx-bob filtered list does not leak Alice's task.

So the issue is not that routa lacks any workspace concept. The issue is that
the exposed A2A task-control routes still provide task-id-only or widened paths
that are not bound to caller authority.

Expected Behavior

Every A2A task read, list, and cancel path should bind the selected task set to
authenticated caller authority, task ownership, or an equivalent workspace
policy before returning or mutating it. Bare taskId or omitted workspace
filters should not be sufficient authority.

Actual Behavior

The A2A bridge treats tasks as globally readable/mutable unless the caller
voluntarily supplies restrictive filters. A second caller can list across
workspaces and then read or cancel another caller's task by task id.

Impact

Depending on deployment and task contents, this can cause:

  • unauthorized cross-workspace task discovery through unfiltered task listing;
  • unauthorized task history/payload disclosure through GetTask;
  • unauthorized task lifecycle mutation through CancelTask;
  • cross-workspace reuse of A2A task handles in a shared product process.

Because routa is a multi-workspace coordination product, this is more than a
generic missing-auth footgun. The app already stores workspace identity and can
filter by it, so the failing behavior is specifically the exposed route/binding
policy on top of that bridge.

Suggested Fix

  • bind task list/get/cancel to authenticated caller authority or enforced
    workspace ownership before the bridge result is returned;
  • avoid exposing an unfiltered "all workspaces" task view on shared A2A routes;
  • treat taskId as an object selector, not an authorization token;
  • add Alice/Bob regression tests for REST and JSON-RPC ListTasks, GetTask,
    and CancelTask;
  • review whether any adjacent broker/operator-plane reads also rely on optional
    caller-supplied filters rather than enforced caller binding.

Attachments

attachments.zip

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions