Skip to content

Cross-team A2A agent takeover via missing ownership check in the admin edit route (POST /admin/a2a/{agent_id}/edit)

High
Lang-Akshay published GHSA-qmfj-jjw4-8qrq Sep 7, 2026

Package

pip mcp-contextforge-gateway (pip)

Affected versions

<= 1.0.6

Patched versions

v1.0.7

Description

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.

Severity

High

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
Low
Privileges required
Low
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
High
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N

CVE ID

No known CVE

Weaknesses

No CWEs

Credits