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:
commit
a6a9d8edf0
@ -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))
|
||||
|
||||
@ -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",
|
||||
)
|
||||
Loading…
Reference in New Issue
Block a user