From 1c3c1af529b6edfc63161c58a7172f72d25ff55b Mon Sep 17 00:00:00 2001 From: Tin Chi Lo Date: Sat, 11 Jul 2026 14:39:57 -0700 Subject: [PATCH] fix(mcp): run proxy-wide pre-DB gates on bridge envelope admission The envelope arm bypasses user_api_key_auth, so it never ran pre_db_read_auth_checks (request-size and body-safety limits, the IP allowlist, and the general_settings route allowlist) that the normal MCP admission path runs before any key lookup. A caller blocked by IP or a disallowed proxy route could be admitted through an envelope where the same principal on the normal path is rejected. Run those gates before the envelope crypto, mirroring the pipeline's pre-DB ordering; a blocked IP or route surfaces its own 403. --- .../mcp_server/auth/user_api_key_auth_mcp.py | 21 ++++++++++++++ .../auth/test_user_api_key_auth_mcp.py | 28 +++++++++++++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py index f63819b1822..e300a22e5db 100644 --- a/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py +++ b/litellm/proxy/_experimental/mcp_server/auth/user_api_key_auth_mcp.py @@ -534,6 +534,8 @@ class MCPRequestHandler: if not master_key: raise HTTPException(status_code=500, detail="Server misconfigured: master_key is not set") + await MCPRequestHandler._run_pre_db_read_auth_checks(request=request, route=route) + keys = envelope_keys_from_master_key(master_key) result = resolve_bridge_envelope(authorization_value, keys, datetime.now(timezone.utc), server.server_id) match result: @@ -551,6 +553,25 @@ class MCPRequestHandler: case _: assert_never(result) + @staticmethod + async def _run_pre_db_read_auth_checks(request: Request, route: str) -> None: + """Run the proxy-wide gates ``user_api_key_auth`` applies before any key lookup: the + request-size and body-safety limits, the IP allowlist, and the ``general_settings`` + route allowlist. The envelope arm bypasses ``user_api_key_auth`` (it opens the envelope + and reloads the identity itself), so without this a caller blocked by IP or hitting a + proxy route the allowlist forbids would be admitted through an envelope where the same + principal presented on the normal MCP admission path would be rejected. Runs before the + envelope crypto so a disallowed caller is turned away before any work, mirroring the + standard pipeline's pre-DB ordering. Violations raise the gate's own status (an IP or + route block is a 403, an oversized body its own limit error).""" + from litellm.proxy.auth.auth_utils import pre_db_read_auth_checks + + await pre_db_read_auth_checks( + request=request, + request_data=await _read_request_body(request=request), + route=route, + ) + @staticmethod async def _reload_admitted_key(key_hash: str) -> UserAPIKeyAuth: """Reload the live key record an admitted envelope references and re-check live policy. diff --git a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py index eefaaf1dc99..c785ac577f7 100644 --- a/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py +++ b/tests/test_litellm/proxy/_experimental/mcp_server/auth/test_user_api_key_auth_mcp.py @@ -5243,9 +5243,7 @@ class TestMCPDcrBridgeDelegateAdmission: assert exc_info.value.status_code == 401 - _POLICY_GATE = ( - "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp._run_centralized_common_checks" - ) + _POLICY_GATE = "litellm.proxy._experimental.mcp_server.auth.user_api_key_auth_mcp._run_centralized_common_checks" async def _enforce_with_gate_error(self, error): """Drive _enforce_admitted_live_policy with the centralized gate raising ``error`` and return @@ -5322,6 +5320,30 @@ class TestMCPDcrBridgeDelegateAdmission: assert exc_info.value.status_code == 403 + async def test_envelope_rejected_by_proxy_wide_pre_db_gates_403(self): + """The envelope arm runs the same proxy-wide pre-DB gates user_api_key_auth applies before any + key lookup (request size, body safety, IP allowlist, general_settings route allowlist). Here + the proxy route allowlist forbids MCP, so the envelope is turned away with a 403 before the + identity is even reloaded, closing the gap where an envelope bypassed the IP/route allowlists + the normal MCP admission path enforces.""" + envelope = self._mint_bridge_envelope(key_hash=self._KEY_HASH) + scope = { + "type": "http", + "method": "POST", + "path": "/mcp/bridge_delegate_server", + "headers": [(b"authorization", f"Bearer {envelope}".encode("latin-1"))], + } + with ( + patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.global_mcp_server_manager") as mock_mgr, + patch("litellm.proxy.proxy_server.master_key", self._MASTER_KEY), + patch("litellm.proxy.proxy_server.general_settings", {"allowed_routes": ["/chat/completions"]}), + ): + mock_mgr.get_mcp_server_by_name.return_value = self._bridge_delegate_server() + with pytest.raises(HTTPException) as exc_info: + await MCPRequestHandler.process_mcp_request(scope) + + assert exc_info.value.status_code == 403 + async def test_blocked_state_bare_exception_stays_401(self): """A blocked team/project raises a bare Exception (no status) in common_checks, which the standard pipeline renders as 401; the arm keeps failing those closed as 401, never a 500."""