CVE Patch Review

CVE-2026-52870 Patch Review: Partial fix for MCP Python SDK task authorization

CVE-2026-52870 · GHSA-HVRP-RF83-W775 · Updated 2026-07-17 Partial fix

Summary

The patch introduces per-session task scoping for SDK-generated task IDs and enforces that scope in the default task handlers for get/result/cancel/list. This materially reduces cross-session task exposure for the common path, but it does not fully eliminate the authorization weakness because explicitly supplied task IDs, TaskStore-created IDs, and stateless-mode tasks remain capability-based and accessible to any requester that knows the ID. The fix is therefore strong for the default session-bound workflow, but not a universal object-authorization model.

Analysis

Vulnerability

CVE-2026-52870 describes a Broken Object-Level Authorization issue in the MCP Python SDK where connected clients could list, retrieve, and cancel background tasks belonging to other sessions because task operations lacked request-context validation. The advisory context in GHSA-HVRP-RF83-W775 and the patch set in PR #2720 show that the vulnerable design treated task identifiers as globally usable across sessions in the default handlers.

The core flaw was not task execution itself, but authorization on task objects. Before the patch, default task endpoints effectively trusted possession of a task ID and did not bind task visibility or mutating operations to the session that created the task. The diff for src/mcp/server/lowlevel/experimental.py shows the remediation focus: adding requestor-scope checks before serving task operations and filtering list responses by session scope. This aligns with the CVE description at CVE.org.

def _require_task_in_requestor_scope(self, task_id: str) -> None:
    if not task_in_session_scope(task_id, self._requestor_session_scope()):
        raise McpError(
            ErrorData(
                code=INVALID_PARAMS,
                message=f"Task not found: {task_id}",
            )
        )

That snippet, from the patched code in commit 62137874ff26dd74d2fea80ff528a7fd9ca7a5e7, demonstrates the intended security property: cross-session tasks should be indistinguishable from nonexistent tasks when using the default handlers.

Patch

The patch introduces a new task-scoping mechanism in src/mcp/server/experimental/task_scope.py. SDK-generated task IDs now embed an opaque per-session marker, and helper functions parse and validate that marker. new_session_scope() creates a random session token, scoped_task_id() prefixes generated task IDs with that token, and task_in_session_scope() / task_listable_in_session_scope() define access semantics for direct access and listing.

Session state is extended in src/mcp/server/experimental/session_features.py with task_session_scope, and TaskSupport.configure_session() now assigns a scope for non-stateless sessions. In request_context.py, run_task() generates a scoped task ID when the caller does not provide one explicitly. The patch also adds a deprecation warning for explicit task_id values because such IDs are used verbatim and are not associated with the creating session.

The default handlers in src/mcp/server/lowlevel/experimental.py are the main enforcement point. They now derive the requestor's session scope, reject task access when the embedded scope belongs to another session, and restrict tasks/list to tasks whose IDs are listable in the current session scope. The docs update in PR #2720 is important because it explicitly documents residual behavior: explicit IDs, TaskStore-created IDs, and stateless-mode tasks remain accessible to any requester that presents the exact ID, and are treated as capabilities rather than session-owned objects.

def task_in_session_scope(task_id: str, session_scope: str | None) -> bool:
    embedded = session_scope_of(task_id)
    return embedded is None or embedded == session_scope


def task_listable_in_session_scope(task_id: str, session_scope: str | None) -> bool:
    embedded = session_scope_of(task_id)
    return embedded is not None and embedded == session_scope

This is a meaningful design change: object access is no longer globally shared for the default generated-ID path, but the patch intentionally preserves capability-style access for unscoped IDs.

Review

Pros

  • The patch addresses the immediate BOLA condition in the default SDK path by binding generated task IDs to a per-session scope and enforcing that scope in get/result/cancel/list handlers.
  • The implementation is internally consistent: ID generation, session initialization, handler enforcement, and documentation were all updated together in the fixing commit.
  • The regex-based scoped ID format reduces ambiguity by ensuring explicitly chosen IDs are not accidentally interpreted as scoped IDs.
  • The error behavior is privacy-aware: cross-session access returns a not-found style response, reducing task enumeration signal.
  • The patch acknowledges pagination leakage in tasks/list and avoids exposing store cursors derived from unfiltered listings, which is a good secondary hardening measure.
  • Deprecating explicit task_id use is a pragmatic compatibility step that warns developers without immediately breaking existing integrations.

Cons

  • Partial fix. The patch does not establish a universal authorization model for tasks; it only scopes SDK-generated IDs in stateful sessions.
  • Unscoped task IDs remain accessible from any session if the requester knows the ID. The docs explicitly preserve this for explicit task_id values, direct TaskStore creation, and stateless mode. That means the system still relies on capability secrecy for some task objects rather than enforcing ownership.
  • Because explicit IDs are only deprecated, not blocked, existing or new applications can still opt into the weaker behavior, intentionally or accidentally.
  • Stateless mode remains a structural exception. The patch explains why session binding is not possible there, but from a security perspective it means the default protection is absent in that deployment model.
  • The documentation advises custom handlers for user-identity scoping, which is correct, but it also means robust authorization is delegated to application authors rather than guaranteed by the SDK defaults in all cases.
  • The review material does not show a migration mechanism for previously created unscoped tasks, so mixed populations of scoped and unscoped task IDs may persist after upgrade.

Verdict

Partial fix.

The patch substantially improves the default behavior and closes the reported cross-session exposure for the common stateful, SDK-generated task-ID path. However, it does not fully eliminate the underlying authorization weakness across all task creation and deployment modes. The patched design explicitly allows unscoped task IDs to remain globally usable by possession, which is a capability model rather than strict object-level authorization. For engineers evaluating remediation completeness, this is a strong mitigation and a correct fix for the default handlers, but not a complete root-cause elimination for every task object lifecycle described in the advisory and the patch discussion.

Recommended follow-up work is to add an opt-in or future default that binds tasks to an application-defined principal rather than only a transport session, and to provide a stricter mode that rejects explicit unscoped IDs entirely. That would move the SDK from session-scoped mitigation toward a more complete authorization model consistent with the BOLA class described by NVD.

Sources