Skip to content

fix(security): enforce authorization for listTasks - #1038

Merged
kabir merged 2 commits into
a2aproject:mainfrom
ez-lbz:fix/listtasks-authz-bypass
Aug 11, 2026
Merged

fix(security): enforce authorization for listTasks#1038
kabir merged 2 commits into
a2aproject:mainfrom
ez-lbz:fix/listtasks-authz-bypass

Conversation

@ez-lbz

@ez-lbz ez-lbz commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What changed

1. Enforce authorization for onListTasks (CWE-862)

Problem: onListTasks in AuthorizationRequestHandlerDecorator delegated directly to the wrapped handler without any authorization check, unlike every other handler method which calls an enforce* check first. With a TaskAuthorizationProvider configured, a caller could invoke listTasks and bypass the authorization model entirely — only per-task filtering in the TaskStore (which custom store implementations may skip) stood between the caller and other users' tasks.

Fix (server-common/src/main/java/org/a2aproject/sdk/server/requesthandlers/AuthorizationRequestHandlerDecorator.java):

  • Added a list-scoped read check (enforceListRead) invoked from onListTasks before delegation. Since LIST_TASKS has no single task ID, the provider is invoked with an empty-string sentinel (LIST_TASKS_SCOPE_ID) representing the whole list scope; a denial throws TaskNotFoundError (the same fail-closed error used by all other checks, avoiding information leakage). A null task ID was deliberately avoided because it breaks providers that key lookups on the task ID (e.g. ConcurrentHashMap-backed stores throw on get(null)).
  • Per-task checkRead filtering in the TaskStore.list() implementation remains in place and is unchanged.

Fix (server-common/src/test/java/org/a2aproject/sdk/server/requesthandlers/AuthorizationRequestHandlerDecoratorTest.java):

  • Added onListTasks_denied — when checkRead(context, "", LIST_TASKS) returns false, the call throws TaskNotFoundError and the delegate is never invoked.
  • Added onListTasks_allowed — when the check returns true, the call is delegated and the result returned.

Behavior change: with a TaskAuthorizationProvider configured, listTasks now goes through a list-scoped authorization gate; a provider that denies LIST_TASKS will reject list requests outright. Providers that allow unknown tasks (e.g. TestTaskAuthorizationProvider, whose lookup for the sentinel ID yields no owner and therefore allows) see no behavior change.

Testing

  • mvn -pl server-common test -Dtest=AuthorizationRequestHandlerDecoratorTest29 tests run, 0 failures, 0 errors (includes the 2 new regression tests).
  • mvn -pl reference/jsonrpc test -Dtest=QuarkusA2AJSONRPCWithTaskAuthorizationVertxTest7 tests run, 0 failures, 0 errors (this suite previously failed in CI with Cannot invoke "Object.hashCode()" because "key" is null; the list-scoped authorization test now passes with the sentinel-based check).
  • CI re-run after the fix: build matrix green.

onListTasks in AuthorizationRequestHandlerDecorator delegated directly
without any authorization check, unlike every other handler method.

Add a list-scoped read check before delegation. The provider is invoked
with an empty-string sentinel for the whole list scope (a null task ID
would break ConcurrentHashMap-backed providers with an NPE on lookup).
Per-task checkRead filtering in the TaskStore list() remains unchanged.

@kabir kabir left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, thank you for the PR :-)

The fix looks good — two documentation points to consider:

  1. The Javadoc in TaskAuthorizationProvider (lines 64-65) still says onListTasks filtering is "pushed down to the TaskStore, which calls
    checkRead per task." That no longer captures the full picture after this PR — the decorator now does a list-scoped check before delegation. Worth
    updating so implementors see both layers.
  2. The checkRead Javadoc describes taskId as "the task being accessed," but this PR calls it with an empty-string sentinel for list-scope
    authorization. Implementors of TaskAuthorizationProvider won't know that checkRead can receive "" with LIST_TASKS, or what to return in that
    case. Documenting the sentinel convention (or introducing a separate method) would prevent providers from silently doing the wrong thing.

…entinel

The onListTasks bullet in TaskAuthorizationProvider's behavior Javadoc no
longer reflects the decorator, which now performs a list-scoped checkRead
before delegation in addition to the per-task TaskStore filtering. Also
document the empty-string sentinel convention for checkRead with
LIST_TASKS so providers know what to return for the list scope.
@ez-lbz

ez-lbz commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Both documentation points are now addressed:

  1. The onListTasks bullet in TaskAuthorizationProvider's behavior Javadoc now describes both layers: the decorator performs a list-scoped checkRead before delegation, and the TaskStore still applies per-task checkRead filtering during list().
  2. checkRead's Javadoc now documents the empty-string sentinel convention for LIST_TASKS — providers know "" + LIST_TASKS means "may this user list tasks at all", and that denying it hides all tasks.

@kabir
kabir merged commit e9a1abf into a2aproject:main Aug 11, 2026
13 checks passed
@kabir

kabir commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Thank you @ez-lbz :-)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants