From 2519ac161e133ff205d90e536ba3432aa6589468 Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 14 May 2026 03:29:16 +0000 Subject: [PATCH 1/3] chore(proxy): cover extra_body + azure_ad_token in banned-params check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``extra_body`` is the OpenAI-SDK passthrough container. Provider modules read provider-auth fields out of it directly (Azure's ``extra_body.azure_ad_token``, Bedrock's ``extra_body.aws_web_identity_token``, etc.) without re-validating, so the boundary check has to walk it the same way it walks ``litellm_embedding_config``. Adding it to ``_NESTED_CONFIG_KEYS`` extends single-level banned-key descent into the container — top-level admin opt-ins (``allow_client_side_credentials`` / ``configurable_clientside_auth_params``) still apply. ``azure_ad_token`` was not in ``_BANNED_REQUEST_BODY_PARAMS`` despite being the bearer-token field the Azure transformer resolves through ``get_secret`` (same shape as ``aws_web_identity_token`` on the Bedrock STS path). Added so it can't be supplied per-request without an admin opt-in. --- litellm/proxy/auth/auth_utils.py | 17 +++- .../auth/test_banned_params_extra_body.py | 82 +++++++++++++++++++ 2 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 tests/test_litellm/proxy/auth/test_banned_params_extra_body.py 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", + ) From 5e56022553df90912f84797296938fae825af6ac Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 14 May 2026 03:37:14 +0000 Subject: [PATCH 2/3] chore(proxy): coerce stringified nested-config containers before descent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ``_NESTED_CONFIG_KEYS`` descent used ``isinstance(nested, dict)``, so a caller sending ``extra_body`` as a JSON-encoded string instead of an object (the same shape multipart/form-data clients use for ``litellm_metadata``) skipped the banned-key check entirely. Switched to ``_coerce_metadata_to_dict`` so the JSON-string path is parsed before descent — mirrors the existing handling on ``_NESTED_METADATA_KEYS``. --- litellm/proxy/auth/auth_utils.py | 4 ++-- .../auth/test_banned_params_extra_body.py | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 5d7a37afd4..8012a9be76 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -352,8 +352,8 @@ def is_request_body_safe( """ _check_banned_params(request_body, general_settings, llm_router, model) for nested_key in _NESTED_CONFIG_KEYS: - nested = request_body.get(nested_key) - if isinstance(nested, dict): + nested = _coerce_metadata_to_dict(request_body.get(nested_key)) + if nested is not None: _check_banned_params(nested, general_settings, llm_router, model) for metadata_key in _NESTED_METADATA_KEYS: metadata = _coerce_metadata_to_dict(request_body.get(metadata_key)) 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 index 37f9e8c952..fd33f22480 100644 --- a/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py +++ b/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py @@ -80,3 +80,25 @@ def test_admin_opt_in_still_permits_extra_body_credentials(): llm_router=None, model="openai/gpt-4", ) + + +def test_banned_param_under_stringified_extra_body_is_rejected(): + # Raw-HTTP and multipart/form-data clients can send ``extra_body`` as + # a JSON-encoded string rather than an object. An ``isinstance(..., + # dict)`` guard on the nested descent would skip such payloads, + # leaving the banned-key check bypassed. Coercion via + # ``_coerce_metadata_to_dict`` closes that variant. + import json + + body = { + "model": "bedrock/anthropic.claude-v2", + "messages": [{"role": "user", "content": "x"}], + "extra_body": json.dumps({"aws_web_identity_token": "anything"}), + } + 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", + ) From 626b768d25311817738fb56b89f08a30cf24fcec Mon Sep 17 00:00:00 2001 From: user <70670632+stuxf@users.noreply.github.com> Date: Thu, 14 May 2026 03:39:15 +0000 Subject: [PATCH 3/3] chore(tests): drop redundant membership check; trim test comment ``test_azure_ad_token_is_in_banned_list`` only asserted tuple membership of a name the parametrized test already exercises end-to-end through ``is_request_body_safe``. Removed. Tightened the admin-opt-in test comment. --- .../proxy/auth/test_banned_params_extra_body.py | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) 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 index fd33f22480..2ccee38628 100644 --- a/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py +++ b/tests/test_litellm/proxy/auth/test_banned_params_extra_body.py @@ -14,10 +14,7 @@ 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, -) +from litellm.proxy.auth.auth_utils import is_request_body_safe # noqa: E402 @pytest.mark.parametrize( @@ -47,10 +44,6 @@ def test_banned_param_under_extra_body_is_rejected(banned_param): ) -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", @@ -66,9 +59,8 @@ def test_extra_body_with_safe_fields_is_allowed(): 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. + # ``allow_client_side_credentials`` is the admin escape; descending + # into ``extra_body`` must preserve it. body = { "model": "openai/gpt-4", "messages": [{"role": "user", "content": "x"}],