Merge pull request #27898 from stuxf/chore/banned-params-extra-body-cover

chore(proxy): cover extra_body + azure_ad_token in banned-params check
This commit is contained in:
yuneng-jiang 2026-05-13 20:48:57 -07:00 committed by GitHub
commit a6a9d8edf0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 112 additions and 5 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
@ -341,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))

View File

@ -0,0 +1,96 @@
"""
``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 is_request_body_safe # noqa: E402
@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_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():
# ``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"}],
"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",
)
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",
)