## Summary The admin A2A-agent edit route does not enforce object ownership
High8.1Published Sep 7, 2026
## Summary The admin A2A-agent edit route does not enforce object ownership. An authenticated non-owner holding the common `a2a.update` permission can take over any A2A agent by its id: reassign ownership to themselves, redirect the agent's endpoint URL to attacker-controlled infrastructure, and relocate the agent into their own team, including across team/tenant boundaries. The equivalent API route `PUT /a2a/{agent_id}` and the admin delete route both correctly enforce ownership. ## Affected code - `POST /admin/a2a/{agent_id}/edit` -> `admin_edit_a2a_agent` (`mcpgateway/admin.py`, ~line 16377) calls `a2a_service.update_agent(...)` without `user_email`. - In `a2a_service.update_agent` (`mcpgateway/services/a2a_service.py`, ~line 1424) the ownership check is gated by `if user_email:` -> `check_resource_ownership`. With `user_email` omitted the check is skipped, the agent is fetched by id alone, and every submitted field is applied via `setattr`. - The handler sets `owner_email` to the caller, so a successful edit reassigns ownership to the caller. - Contrast: `PUT /a2a/{agent_id}` (`mcpgateway/main.py`, ~line 5084) and the admin delete route both pass `user_email` and enforce own...
Affected versions
| Package | Affected | Fixed in |
|---|---|---|
| mcp-contextforge-gateway PyPI | < v1.0.7 | v1.0.7 |
Details and references
## Summary The admin A2A-agent edit route does not enforce object ownership. An authenticated non-owner holding the common `a2a.update` permission can take over any A2A agent by its id: reassign ownership to themselves, redirect the agent's endpoint URL to attacker-controlled infrastructure, and relocate the agent into their own team, including across team/tenant boundaries. The equivalent API route `PUT /a2a/{agent_id}` and the admin delete route both correctly enforce ownership. ## Affected code - `POST /admin/a2a/{agent_id}/edit` -> `admin_edit_a2a_agent` (`mcpgateway/admin.py`, ~line 16377) calls `a2a_service.update_agent(...)` without `user_email`. - In `a2a_service.update_agent` (`mcpgateway/services/a2a_service.py`, ~line 1424) the ownership check is gated by `if user_email:` -> `check_resource_ownership`. With `user_email` omitted the check is skipped, the agent is fetched by id alone, and every submitted field is applied via `setattr`. - The handler sets `owner_email` to the caller, so a successful edit reassigns ownership to the caller. - Contrast: `PUT /a2a/{agent_id}` (`mcpgateway/main.py`, ~line 5084) and the admin delete route both pass `user_email` and enforce ownership. This is an isolated omission on the admin edit route. - The decorator `require_permission("a2a.update", allow_admin_bypass=False)` only checks that the caller holds `a2a.update`; for a single-team-scoped API token it does not re-scope to the target agent's team, so a cross-team caller is not blocked at the decorator. ## Proof of concept Verified live on v1.0.6 (default multi-tenant: EMAIL_AUTH + RBAC + Teams, sqlite). All actors are non-admin users holding the built-in team `developer` role, which includes `a2a.update`. Setup: team T with `userA` (agent owner) and `userB` (member, not owner); team T2 with `userC`. `userA` creates an A2A agent in team T. 1. Control (same-team non-owner, proper API path): `PUT /a2a/{id}` as `userB` -> `403 {"detail":"Only the owner can update this agent"}` (agent unchanged). 2. Bug (same-team non-owner, admin route): `POST /admin/a2a/{id}/edit` as `userB` with `endpoint_url=https://attacker.example` -> `200 {"success":true}`. Database after: `owner_email` flips `userA -> userB`; `endpoint_url` -> attacker URL. 3. Cross-team: mint a single-team-scoped API token for `userC` (`POST /tokens/teams/{T2}`; the JWT `teams` claim = `["T2"]` only). `POST /admin/a2a/{id}/edit` as `userC` with `team_id=T2` -> `200`. `userA`'s agent (team T) is taken over: `owner_email -> userC`, `endpoint_url -> attacker`, and the agent is relocated into team T2. The same `userC` on `PUT /a2a/{id}` -> `403 {"detail":"Access denied"}`. The differential (the same non-owner is `403` on the API path but `200` on the admin path, with an observed database change) confirms the admin edit route skips the ownership check that the other routes enforce. ## Impact Broken object-level authorization / cross-tenant takeover of A2A agents. An authenticated user with `a2a.update` (a common team role), knowing a target agent's id, can seize ownership, redirect the agent's endpoint URL to attacker-controlled infrastructure (capturing passthrough headers/credentials routed to that agent), and relocate the agent into their own team, breaking team/tenant isolation. ## Suggested remediation Make `admin_edit_a2a_agent` pass `user_email` into `a2a_service.update_agent` so the ownership gate matches the `PUT /a2a/{id}` and admin delete routes. Additionally, scope the `a2a.update` permission check on the admin route to the target agent's team.
- CVSS 3.1
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N
- Severity from
- GitHub (reviewed advisory)
More IBM advisories
All IBM| Date | Advisory | Severity | Fixed in |
|---|---|---|---|
| Sep 8 | IBM Enterprise Build of Quarkus: insecure direct object reference | High7.4 | No fix yet |
| Sep 8 | IBM Enterprise Build of Quarkus: improper access control | Medium5.3 | No fix yet |
| Sep 4 | IBM i: improper authorization | High8.1 | No fix yet |
| Sep 4 | IBM i: improper authentication | High8.1 | No fix yet |
| Sep 4 | IBM i: integer overflow | Medium6.3 | No fix yet |
| Sep 4 | IBM ContextForge MCP Gateway: privilege escalation | High8.8 | No fix yet |