CVE-2026-52869
Improper session validation allows cross-user message injection in MCP SDK.
- CVSS 7.1
- CWE-639
- Authentication, Authorization, and Session Management
- Remote
The MCP Python SDK, called mcp on PyPI, is a Python implementation of the Model Context Protocol (MCP). Prior to 1.27.2, the SSE and stateful Streamable HTTP transports mcp.server.sse.SseServerTransport and mcp.server.streamable_http_manager.StreamableHTTPSessionManager route requests to existing sessions using only the session_id query parameter or Mcp-Session-Id header without verifying the authenticated principal that created the session, allowing a different bearer-token-authenticated client with a known session ID to inject JSON-RPC messages into that session. This issue is fixed in version 1.27.2.
- CWE
- CWE-639
- CVSS base score
- 7.1
- Published
- 2026-07-15
- OWASP
- A01 Broken Access Control
- Orthogonal defect classification
- Checking
- Code defect classification
- Missing Check
- Category
- Authentication, Authorization, and Session Management
- Subcategory
- Insecure Direct Object References (IDOR)
- Accessibility scope
- Remote
- Impact
- Privilege Escalation
- Affected component
- mcp
- Fixed by upgrading
- Yes
Solution
Upgrade mcp to version 1.27.2 or later.
Vulnerable code sample
import uuid
from flask import Flask, request, jsonify
app = Flask(__name__)
SESSIONS = {}
USERS = {
'victim-token-secret': 'user_victim',
'attacker-token-secret': 'user_attacker',
}
def get_principal_from_request(req):
"""Simulates authenticating a user via a bearer token."""
auth_header = req.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return None
token = auth_header.split(' ')[1]
return USERS.get(token)
@app.route('/mcp/session', methods=['POST'])
def create_session():
"""Creates a new session for the authenticated user."""
principal = get_principal_from_request(request)
if not principal:
return jsonify({"error": "Unauthorized"}), 401
session_id = str(uuid.uuid4())
SESSIONS[session_id] = {
'owner': principal,
'messages': []
}
return jsonify({"session_id": session_id}), 201
@app.route('/mcp/rpc', methods=['POST'])
def handle_rpc_message():
"""
VULNERABLE ENDPOINT: Routes a message to a session.
It authenticates the client but does not verify that the authenticated
principal is the owner of the target session.
"""
principal = get_principal_from_request(request)
if not principal:
return jsonify({"error": "Unauthorized"}), 401
session_id = request.args.get('session_id') or request.headers.get('Mcp-Session-Id')
if not session_id:
return jsonify({"error": "session_id is required"}), 400
# --- VULNERABILITY ---
# The session is retrieved using only the session_id.
session = SESSIONS.get(session_id)
if not session:
return jsonify({"error": "Session not found"}), 404
# CRITICAL FLAW: The code does NOT check if the current principal
# (e.g., 'user_attacker') is the same as the session's owner
# (e.g., 'user_victim').
# A correct implementation would add a check here like:
# if session['owner'] != principal:
# return jsonify({"error": "Forbidden"}), 403
# The message is processed and added to the session, regardless of ownership.
message_data = request.json
session['messages'].append(message_data)
return jsonify({"status": "message processed"}), 200Patched code sample
import sys
from dataclasses import dataclass
from typing import Dict, Any
# This code represents a hypothetical server fixing a session validation vulnerability.
# The original vulnerability (described in the fictitious CVE-2026-52869)
# would route requests to a session using only the session_id, without
# checking if the currently authenticated user (the principal) is the one
# who created the session.
# --- Data models to represent server state and requests ---
@dataclass
class Session:
"""Represents a session created by a specific user."""
session_id: str
owner_principal: str # The authenticated user who created the session, e.g., 'user_alice'
data: Dict[str, Any]
@dataclass
class IncomingRequest:
"""Represents a request from a client."""
session_id_from_header: str
authenticated_principal: str # The user authenticated for this request, e.g., 'user_bob'
message_to_inject: str
# --- Mock server session store ---
# A mock database of active sessions. In a real application, this would be
# a more robust storage system (like Redis or a database).
SESSIONS: Dict[str, Session] = {
"session-abc-123": Session(
session_id="session-abc-123",
owner_principal="user_alice",
data={"messages": []}
)
}
# --- The request handler function containing the fix ---
def handle_session_message_fixed(request: IncomingRequest, session_store: Dict[str, Session]):
"""
Handles an incoming message for a session, with the vulnerability fixed.
"""
# Step 1: Retrieve the session using the session ID from the request.
# This step was also present in the vulnerable version.
target_session = session_store.get(request.session_id_from_header)
if not target_session:
raise ValueError(f"Session '{request.session_id_from_header}' not found.")
# --- START OF VULNERABILITY FIX ---
# The vulnerable version would skip this check and proceed directly to
# processing the message, allowing any authenticated user to access the session.
#
# The fix is to verify that the principal authenticated for the current
# request matches the principal that originally created the session.
if target_session.owner_principal != request.authenticated_principal:
# If the principals do not match, authoritatively deny access.
raise PermissionError(
f"Access denied. Principal '{request.authenticated_principal}' "
f"does not own session '{target_session.session_id}'."
)
# --- END OF VULNERABILITY FIX ---
# If the check passes, it is now safe to process the message and modify the session.
print(
f"Success: Principal '{request.authenticated_principal}' is authorized. "
f"Injecting message into session '{target_session.session_id}'.",
file=sys.stderr
)
target_session.data["messages"].append(request.message_to_inject)
if __name__ == '__main__':
print("--- Demonstrating the fix for CVE-2026-52869 ---\n")
# Scenario 1: The legitimate owner ('user_alice') sends a message.
# This should succeed.
legitimate_request = IncomingRequest(
session_id_from_header="session-abc-123",
authenticated_principal="user_alice",
message_to_inject="Hello from Alice!"
)
print("Attempt 1: Legitimate owner 'user_alice' accesses her own session.")
try:
handle_session_message_fixed(legitimate_request, SESSIONS)
print("Result: OK. Message injected successfully.\n")
except (ValueError, PermissionError) as e:
print(f"Result: FAILED. Unexpected error: {e}\n")
# Scenario 2: An attacker ('user_bob') knows the session ID and tries to
# inject a message.
# The vulnerable code would have allowed this. The fixed code must block it.
malicious_request = IncomingRequest(
session_id_from_header="session-abc-123",
authenticated_principal="user_bob", # Different principal!
message_to_inject="Malicious message from Bob!"
)
print("Attempt 2: Attacker 'user_bob' tries to access 'user_alice' session.")
try:
handle_session_message_fixed(malicious_request, SESSIONS)
print("Result: FAILED. The vulnerability is still present.\n")
except PermissionError as e:
print(f"Result: OK. The fix worked. The server raised an error: {e}\n")
print(f"Final session state: {SESSIONS['session-abc-123']}")Payload
curl -X POST 'https://mcp-server.example.com/sse_endpoint?session_id=KNOWN_VICTIM_SESSION_ID' \
-H 'Authorization: Bearer ATTACKER_AUTHENTICATION_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc": "2.0",
"method": "executeAction",
"params": {
"command": "delete_user_data"
},
"id": "attacker-request-1"
}'
Cite this entry
@misc{vaitp:cve202652869,
title = {{Improper session validation allows cross-user message injection in MCP SDK.}},
author = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
year = {2026},
note = {VAITP Python Vulnerability Dataset, entry CVE-2026-52869},
howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-52869/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.
The supreme art of war is to subdue the enemy without fighting.
Sun Tzu – “The Art of War”
:: Shaping the future through research and ingenuity ::
