fix(mcp): fall back to client_ip in stateful session owner fingerprint

Addresses Greptile review on PR #26857: when no API key, user_id, or
OAuth bearer is available (e.g. unauthenticated/passthrough callers),
the owner fingerprint collapsed to a single 'anonymous' value, allowing
two unrelated callers to drive each other's stateful MCP sessions.

Fold client IP into the fingerprint as a fallback identity signal so
distinct anonymous sources do not share an owner identity.

Co-authored-by: Mateo Wang <mateo-berri@users.noreply.github.com>
This commit is contained in:
Cursor Agent 2026-05-06 00:25:58 +00:00
parent 356b8af65d
commit ea0a12f9ba
No known key found for this signature in database
2 changed files with 27 additions and 2 deletions

View file

@ -2674,6 +2674,7 @@ if MCP_AVAILABLE:
def _owner_fingerprint_for(
user_api_key_auth: Optional[UserAPIKeyAuth],
oauth2_headers: Optional[Dict[str, str]] = None,
client_ip: Optional[str] = None,
) -> str:
"""
Stable, non-reversible identifier for the caller used to bind an
@ -2684,6 +2685,13 @@ if MCP_AVAILABLE:
the caller's identity is the upstream OAuth bearer; hash it so two
OAuth callers with different tokens don't both fingerprint to
``anonymous`` and end up sharing a session.
When no caller-identifying credentials are available at all
(e.g. proxy running without master key, or an unauthenticated
passthrough path), fall back to the client IP so two unrelated
anonymous callers from different sources do not collapse to a
single ``anonymous`` owner and end up able to drive each other's
stateful sessions.
"""
if user_api_key_auth is not None:
if user_api_key_auth.api_key:
@ -2696,6 +2704,8 @@ if MCP_AVAILABLE:
)
if authz:
return f"oauth:{hashlib.sha256(authz.encode('utf-8')).hexdigest()}"
if client_ip:
return f"ip:{hashlib.sha256(client_ip.encode('utf-8')).hexdigest()}"
return "anonymous"
def _is_initialize_request(body: bytes) -> bool:
@ -3003,7 +3013,7 @@ if MCP_AVAILABLE:
if session_id:
expected_owner = _stateful_session_owners.get(session_id)
request_owner = _owner_fingerprint_for(
user_api_key_auth, oauth2_headers
user_api_key_auth, oauth2_headers, _client_ip
)
if expected_owner is not None and expected_owner != request_owner:
verbose_logger.warning(
@ -3074,7 +3084,9 @@ if MCP_AVAILABLE:
local_send = _wrap_send_with_stateful_session_auth_context(
local_send,
auth_user,
_owner_fingerprint_for(user_api_key_auth, oauth2_headers),
_owner_fingerprint_for(
user_api_key_auth, oauth2_headers, _client_ip
),
)
async with _gateway_initialize_instructions_request_scope(

View file

@ -1453,6 +1453,19 @@ async def test_owner_fingerprint_distinguishes_oauth_callers():
assert fp_no_oauth == "anonymous"
assert "Bearer token-A" not in fp_a
# When no API key, user_id, or OAuth bearer is available, fall back to
# client IP so two unrelated unauthenticated callers from different
# sources don't collapse to a single 'anonymous' owner and end up able
# to drive each other's stateful sessions.
fp_ip_a = _owner_fingerprint_for(anon_auth, None, "10.0.0.1")
fp_ip_b = _owner_fingerprint_for(anon_auth, None, "10.0.0.2")
fp_ip_a_again = _owner_fingerprint_for(anon_auth, None, "10.0.0.1")
assert fp_ip_a != fp_ip_b
assert fp_ip_a == fp_ip_a_again
assert fp_ip_a.startswith("ip:")
assert "10.0.0.1" not in fp_ip_a
@pytest.mark.asyncio
async def test_stateful_mcp_session_owner_mismatch_returns_403():