Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions litellm/proxy/auth/auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -246,6 +250,13 @@ def _build_banned_observability_params() -> FrozenSet[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
Expand Down Expand Up @@ -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))
Expand Down
96 changes: 96 additions & 0 deletions tests/test_litellm/proxy/auth/test_banned_params_extra_body.py
Original file line number Diff line number Diff line change
@@ -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