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:
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:
- Alice creates a task with
workspaceId=ws-alice and contextId=ctx-alice.
- Bob creates a separate task in
ws-bob.
- Bob lists tasks without a workspace filter and sees Alice's task.
- Bob reads Alice's task by bare task id through the same bridge.
- 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
Summary
At
phodal/routacommit73eb1ed1b719, the application exposes real A2A tasksurfaces over both REST and JSON-RPC while storing tasks in a process-global
A2ATaskBridge.The bridge accepts caller-selected
workspaceIdandcontextId, persiststhose 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/taskswidens across workspaces when no workspace filter issupplied;
GET /api/a2a/tasks/{id}reads a task by bare id;POST /api/a2a/tasks/{id}cancels a task by bare id;tasks/getandtasks/cancelalso act by bare task id.The authority path is:
Affected Target Verified
https://github.com/phodal/routa73eb1ed1b719src/core/a2a/a2a-task-bridge.tssrc/app/api/a2a/tasks/route.tssrc/app/api/a2a/tasks/[id]/route.tssrc/app/api/a2a/rpc/route.tsA2A bridge and route logic
Preconditions
/api/a2a/*REST and/or JSON-RPC routes.upstream agent peer.
in normal A2A responses and list routes.
Steps To Reproduce
From this report directory:
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:
workspaceId=ws-aliceandcontextId=ctx-alice.ws-bob.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:
createTask()preserves caller-selectedworkspaceIdandcontextId;listTasks()filters by workspace only when a workspace filter is supplied;ListTaskswidens to every workspace when no filter is supplied;GetTaskandCancelTaskload from the bridge by bare task id;GetTaskandCancelTaskalso load from the bridge by bare taskid.
The negative controls are important:
workspaceId=ws-bobfiltered list does not leak Alice's task;contextId=ctx-bobfiltered list does not leak Alice's task.So the issue is not that
routalacks any workspace concept. The issue is thatthe 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
taskIdor omitted workspacefilters 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:
GetTask;CancelTask;Because
routais a multi-workspace coordination product, this is more than ageneric 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
workspace ownership before the bridge result is returned;
taskIdas an object selector, not an authorization token;ListTasks,GetTask,and
CancelTask;caller-supplied filters rather than enforced caller binding.
Attachments
attachments.zip