mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
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.
This commit is contained in:
parent
688f535bbf
commit
1c3c1af529
2 changed files with 46 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue