Key cached MCP clients by a digest of the whole token

The cache key held the first ten characters of a bearer token or API key.
Every fine-grained GitHub token starts with github_pat_, so two users of
the same server shared one cached client, and with it the first user's
token, for up to five minutes.
This commit is contained in:
arc53-machine committed 2026-09-29 10:41:03 +01:00
1 parent f4397a7a65
commit 35d6d80e87
2 files changed
+30 -2

No files matched your search

+13 -2
View File
@@ -1,6 +1,7 @@
import asyncio
import base64
import concurrent.futures
import hashlib
import json
import logging
import time
@@ -39,6 +40,16 @@ _mcp_clients_cache = {}
_ANNOTATION_HINTS = ("readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint")
def _secret_fingerprint(secret: str) -> str:
"""A short digest of a whole secret, for telling cached clients apart.
A prefix of the secret itself is not enough: every fine-grained GitHub
token starts with ``github_pat_``, so two users' tokens would share (and
reuse) one cached client carrying the first user's token.
"""
return hashlib.sha256(secret.encode("utf-8")).hexdigest()[:16]
def _annotation_hints(annotations: Any) -> Dict[str, bool]:
"""The boolean MCP tool annotation hints, from a model or a dict."""
if annotations is None:
@@ -162,10 +173,10 @@ class MCPTool(Tool):
token = self.auth_credentials.get(
"bearer_token", ""
) or self.auth_credentials.get("access_token", "")
auth_key = f"bearer:{token[:10]}..." if token else "bearer:none"
auth_key = f"bearer:{_secret_fingerprint(token)}" if token else "bearer:none"
elif self.auth_type == "api_key":
api_key = self.auth_credentials.get("api_key", "")
auth_key = f"apikey:{api_key[:10]}..." if api_key else "apikey:none"
auth_key = f"apikey:{_secret_fingerprint(api_key)}" if api_key else "apikey:none"
elif self.auth_type == "basic":
username = self.auth_credentials.get("username", "")
auth_key = f"basic:{username}"
+17
View File
@@ -266,6 +266,23 @@ class TestGenerateCacheKey:
})
assert "apikey:none" in tool._cache_key
@pytest.mark.parametrize("auth_type, field", [("bearer", "bearer_token"), ("api_key", "api_key")])
def test_tokens_with_a_shared_prefix_get_their_own_client(self, auth_type, field):
"""Every fine-grained GitHub token starts with ``github_pat_``; two users' tokens
must never share a cached client (it carries the first user's token)."""
first = _make_tool({
"server_url": "https://mcp.example.com",
"auth_type": auth_type,
"auth_credentials": {field: "github_pat_11AAAAAAA_alice"},
})
second = _make_tool({
"server_url": "https://mcp.example.com",
"auth_type": auth_type,
"auth_credentials": {field: "github_pat_11AAAAAAA_bob"},
})
assert first._cache_key != second._cache_key
assert "github_pat" not in first._cache_key
# =====================================================================
# Transport Creation