diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 567b8307af..5d7a37afd4 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -169,9 +169,13 @@ def _allow_model_level_clientside_configurable_parameters( # Config dicts whose entries are spread as ``**dict`` into outbound LLM # API calls. ``litellm_embedding_config`` is consumed by the Milvus -# vector store transformer; future nested-config keys with the same -# threat shape should be added here. -_NESTED_CONFIG_KEYS: Tuple[str, ...] = ("litellm_embedding_config",) +# vector store transformer. ``extra_body`` is the OpenAI-SDK passthrough +# container: provider modules pull provider-auth fields out of it +# (e.g. Azure's ``extra_body.azure_ad_token``, Bedrock's +# ``extra_body.aws_web_identity_token``) without re-validating, so the +# banned-key check has to descend into it the same way it descends into +# ``litellm_embedding_config``. +_NESTED_CONFIG_KEYS: Tuple[str, ...] = ("litellm_embedding_config", "extra_body") # Metadata containers that carry per-request configuration consumed by the # observability callbacks. The same banned-param list applies — a value @@ -246,6 +250,13 @@ _BANNED_REQUEST_BODY_PARAMS: Tuple[str, ...] = ( "aws_web_identity_token", "aws_role_name", "vertex_credentials", + # Azure managed-identity / federated-auth token. The Azure provider + # transformer reads ``azure_ad_token`` (top-level or via + # ``extra_body``) and resolves it through ``get_secret`` before + # passing it as the bearer token to the Azure endpoint, so a + # caller-supplied value is the same exfil shape as + # ``aws_web_identity_token`` on the Bedrock path. + "azure_ad_token", # Endpoint-targeting fields that retarget the outbound request or # an observability callback. An attacker-controlled value either # exfiltrates the request payload (incl. messages + admin-set diff --git a/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py b/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py new file mode 100644 index 0000000000..37f9e8c952 --- /dev/null +++ b/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py @@ -0,0 +1,82 @@ +""" +``extra_body`` is the OpenAI-SDK passthrough container — provider modules +pull provider-auth fields out of it without re-validating. Without +descending into it, the banned-param boundary check is bypassed by +nesting the same fields under ``extra_body``. +""" + +import os +import sys + +import pytest + +sys.path.insert( + 0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../../..")) +) + +from litellm.proxy.auth.auth_utils import ( # noqa: E402 + _BANNED_REQUEST_BODY_PARAMS, + is_request_body_safe, +) + + +@pytest.mark.parametrize( + "banned_param", + [ + "aws_web_identity_token", + "aws_sts_endpoint", + "aws_role_name", + "api_base", + "base_url", + "vertex_credentials", + "azure_ad_token", + ], +) +def test_banned_param_under_extra_body_is_rejected(banned_param): + body = { + "model": "bedrock/anthropic.claude-v2", + "messages": [{"role": "user", "content": "x"}], + "extra_body": {banned_param: "anything-attacker-chose"}, + } + with pytest.raises(ValueError, match="not allowed in request body"): + is_request_body_safe( + request_body=body, + general_settings={}, + llm_router=None, + model="bedrock/anthropic.claude-v2", + ) + + +def test_azure_ad_token_is_in_banned_list(): + assert "azure_ad_token" in _BANNED_REQUEST_BODY_PARAMS + + +def test_extra_body_with_safe_fields_is_allowed(): + body = { + "model": "openai/gpt-4", + "messages": [{"role": "user", "content": "x"}], + "extra_body": {"reasoning_effort": "low", "seed": 42}, + } + assert is_request_body_safe( + request_body=body, + general_settings={}, + llm_router=None, + model="openai/gpt-4", + ) + + +def test_admin_opt_in_still_permits_extra_body_credentials(): + # ``general_settings.allow_client_side_credentials`` is the documented + # admin escape for clientside-credential passthrough. Walking + # ``extra_body`` for banned params must not break the escape. + body = { + "model": "openai/gpt-4", + "messages": [{"role": "user", "content": "x"}], + "extra_body": {"api_base": "https://my-private-openai.internal"}, + } + assert is_request_body_safe( + request_body=body, + general_settings={"allow_client_side_credentials": True}, + llm_router=None, + model="openai/gpt-4", + )