mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-21 00:21:49 +00:00
fix(mcp): reject bare schemes in raw authorization
This commit is contained in:
parent
6c517bfc49
commit
70ef8b24b6
4 changed files with 46 additions and 13 deletions
|
|
@ -397,7 +397,7 @@ _STATIC_MODES: Final = frozenset(
|
|||
def _usable_credential_value(auth_type: MCPAuthType, name: str, value: str) -> bool:
|
||||
if not value:
|
||||
return False
|
||||
if auth_type == MCPAuth.authorization or (auth_type == MCPAuth.api_key and name != "authorization"):
|
||||
if auth_type == MCPAuth.api_key and name != "authorization":
|
||||
return True
|
||||
if value.lower() in ("bearer", "basic", "token", "apikey"):
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ from litellm.types.mcp_server.mcp_server_manager import MCPServer
|
|||
(MCPAuth.api_key, "Authorization", "Custom Custom"),
|
||||
(MCPAuth.api_key, "X-API-Key", "Bearer Bearer"),
|
||||
(MCPAuth.api_key, "X-Custom", "ApiKey ApiKey"),
|
||||
(MCPAuth.authorization, "Authorization", "Bearer Bearer"),
|
||||
(MCPAuth.authorization, "Authorization", "opaque-secret-value"),
|
||||
])
|
||||
def test_static_credential_preserves_supported_api_key_and_raw_headers(
|
||||
auth_type: MCPAuthType, header: str, value: str,
|
||||
|
|
|
|||
|
|
@ -13577,19 +13577,46 @@ class TestProtectedCredentialPreparation:
|
|||
assert exc.value.status_code in (401, 500)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("auth_type,slot", [(MCPAuth.api_key, "X-API-Key"), (MCPAuth.authorization, "Authorization")])
|
||||
async def test_raw_static_value_named_token_is_a_usable_credential(self, auth_type: MCPAuthType, slot: str) -> None:
|
||||
@pytest.mark.parametrize("auth_type,slot,value", [
|
||||
(MCPAuth.api_key, "X-API-Key", "token"),
|
||||
(MCPAuth.authorization, "Authorization", "opaque-secret-value"),
|
||||
(MCPAuth.authorization, "Authorization", "Bearer abc"),
|
||||
(MCPAuth.authorization, "Authorization", "Custom abc"),
|
||||
])
|
||||
async def test_raw_static_credentials_are_forwarded_unchanged(
|
||||
self, auth_type: MCPAuthType, slot: str, value: str,
|
||||
) -> None:
|
||||
server = MCPServer(server_id="raw-key", name="raw-key", url="https://upstream.example/mcp",
|
||||
transport=MCPTransport.http, auth_type=auth_type, authentication_token="token")
|
||||
transport=MCPTransport.http, auth_type=auth_type, authentication_token=value)
|
||||
client = await MCPServerManager()._create_mcp_client(server)
|
||||
assert client._resolved_auth is not None
|
||||
request = httpx.Request("GET", server.url)
|
||||
flow = client._resolved_auth.auth_flow(request)
|
||||
try:
|
||||
assert next(flow).headers[slot] == "token"
|
||||
assert next(flow).headers[slot] == value
|
||||
finally:
|
||||
flow.close()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("value", ["Bearer", "basic", "token", "ApiKey", " bEaReR ", "\tTOKEN\t"])
|
||||
@pytest.mark.parametrize("source", ["configured", "caller", "forwarded"])
|
||||
async def test_raw_authorization_rejects_bare_schemes_before_dispatch(
|
||||
self, respx_mock: MockRouter, value: str, source: str,
|
||||
) -> None:
|
||||
server: Final = MCPServer(
|
||||
server_id="raw-empty", name="raw-empty", url="https://upstream.example/mcp",
|
||||
transport=MCPTransport.http, auth_type=MCPAuth.authorization,
|
||||
authentication_token=value if source == "configured" else None,
|
||||
)
|
||||
destination: Final = respx_mock.route().respond(200)
|
||||
with pytest.raises(HTTPException, match="requires a usable upstream credential") as exc:
|
||||
await MCPServerManager()._create_mcp_client(
|
||||
server, mcp_auth_header=value if source == "caller" else None,
|
||||
extra_headers={"Authorization": value} if source == "forwarded" else None,
|
||||
)
|
||||
assert exc.value.status_code == 500
|
||||
assert destination.call_count == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_byok_flag_cannot_bypass_incomplete_obo(self) -> None:
|
||||
server = MCPServer(server_id="obo-byok", name="obo-byok", url="https://upstream.example/mcp",
|
||||
|
|
|
|||
|
|
@ -41,17 +41,23 @@ GET_ASYNC_CLIENT_TARGET = "litellm.proxy._experimental.mcp_server.openapi_to_mcp
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize("value,accepted", [
|
||||
("Bearer Bearer", False), ("ApiKey ApiKey", False), ("token token", False),
|
||||
("bEaReR BEARER", False), ("aPiKeY\tAPIKEY", False),
|
||||
("Bearer fixture-key", True), ("ApiKey fixture-key", True), ("token fixture-key", True),
|
||||
@pytest.mark.parametrize("auth_type,value,accepted", [
|
||||
(MCPAuth.api_key, "Bearer Bearer", False), (MCPAuth.api_key, "ApiKey ApiKey", False),
|
||||
(MCPAuth.api_key, "token token", False), (MCPAuth.api_key, "bEaReR BEARER", False),
|
||||
(MCPAuth.api_key, "aPiKeY\tAPIKEY", False), (MCPAuth.api_key, "Bearer fixture-key", True),
|
||||
(MCPAuth.api_key, "ApiKey fixture-key", True), (MCPAuth.api_key, "token fixture-key", True),
|
||||
(MCPAuth.authorization, "Bearer", False), (MCPAuth.authorization, "basic", False),
|
||||
(MCPAuth.authorization, "token", False), (MCPAuth.authorization, "ApiKey", False),
|
||||
(MCPAuth.authorization, " bEaReR ", False), (MCPAuth.authorization, "\tTOKEN\t", False),
|
||||
(MCPAuth.authorization, "opaque-secret-value", True), (MCPAuth.authorization, "Bearer abc", True),
|
||||
(MCPAuth.authorization, "Custom abc", True),
|
||||
])
|
||||
async def test_api_key_authorization_validates_payload_before_http(
|
||||
respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch, value: str, accepted: bool,
|
||||
async def test_authorization_validates_credentials_before_http(
|
||||
respx_mock: MockRouter, monkeypatch: pytest.MonkeyPatch, auth_type: MCPAuthType, value: str, accepted: bool,
|
||||
) -> None:
|
||||
monkeypatch.setenv("DISABLE_AIOHTTP_TRANSPORT", "True")
|
||||
tool: Final = create_tool_function(
|
||||
"/echo", "get", {}, "https://upstream.example", auth_type=MCPAuth.api_key,
|
||||
"/echo", "get", {}, "https://upstream.example", auth_type=auth_type,
|
||||
)
|
||||
destination: Final = respx_mock.get("https://upstream.example/echo").respond(200, text="authenticated")
|
||||
caller_token: Final = _request_auth_header.set(value)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue