chore(proxy): cover extra_body + azure_ad_token in banned-params check

``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.
This commit is contained in:
user 2026-05-14 03:29:16 +00:00
parent 1294165768
commit 2519ac161e
No known key found for this signature in database
2 changed files with 96 additions and 3 deletions

View File

@ -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

View File

@ -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",
)