mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-05 02:41:56 +00:00
fix(fallbacks): gate x-litellm-* passthrough to trusted callers only
The previous patch unconditionally let `x-litellm-*` keys bypass the `llm_provider-` prefix in `process_response_headers`. That function is also called on raw upstream-provider response headers (e.g. from `llm_http_handler.py`), so a malicious provider could return `x-litellm-attempted-fallbacks` and spoof a LiteLLM-internal marker, bypassing the proxy model-override guard. Add a `preserve_litellm_internal_headers` flag (default False). Only `response_metadata.py`, which re-processes the already-built `_hidden_params["additional_headers"]` dict (LiteLLM-owned), passes True. Raw provider header callsites keep the default False, so upstream `x-litellm-*` still gets the `llm_provider-` prefix. Adds a regression test for the spoofing case and renames the existing preserve test to make the trusted-path semantics explicit.
This commit is contained in:
parent
366d8d1db9
commit
068e4ade1b
3 changed files with 41 additions and 8 deletions
|
|
@ -242,7 +242,18 @@ def _get_parent_otel_span_from_kwargs(
|
|||
return None
|
||||
|
||||
|
||||
def process_response_headers(response_headers: Union[httpx.Headers, dict]) -> dict:
|
||||
def process_response_headers(
|
||||
response_headers: Union[httpx.Headers, dict],
|
||||
preserve_litellm_internal_headers: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
`preserve_litellm_internal_headers` must only be True when the input dict is
|
||||
a LiteLLM-owned structure (e.g. `_hidden_params["additional_headers"]` that
|
||||
has already been through one round of processing). For raw upstream
|
||||
provider headers it must remain False, otherwise a malicious provider
|
||||
returning `x-litellm-*` could spoof LiteLLM-internal markers
|
||||
(e.g. `x-litellm-attempted-fallbacks`).
|
||||
"""
|
||||
from litellm.types.utils import OPENAI_RESPONSE_HEADERS
|
||||
|
||||
openai_headers = {}
|
||||
|
|
@ -256,7 +267,7 @@ def process_response_headers(response_headers: Union[httpx.Headers, dict]) -> di
|
|||
"llm_provider-"
|
||||
): # return raw provider headers (incl. openai-compatible ones)
|
||||
processed_headers[k] = v
|
||||
elif k.startswith("x-litellm-"):
|
||||
elif preserve_litellm_internal_headers and k.startswith("x-litellm-"):
|
||||
# LiteLLM's own internal headers (e.g. x-litellm-attempted-fallbacks,
|
||||
# x-litellm-model-group) are not LLM provider headers and must not be
|
||||
# prefixed. Downstream consumers (proxy override, callers checking
|
||||
|
|
|
|||
|
|
@ -49,7 +49,8 @@ class ResponseMetadata:
|
|||
result=self.result, litellm_model_name=model, router_model_id=model_id
|
||||
),
|
||||
"additional_headers": process_response_headers(
|
||||
self._get_value_from_hidden_params("additional_headers") or {}
|
||||
self._get_value_from_hidden_params("additional_headers") or {},
|
||||
preserve_litellm_internal_headers=True,
|
||||
),
|
||||
"litellm_model_name": model,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,20 +109,41 @@ async def test_async_completion_with_fallbacks_header_is_zero_when_primary_succe
|
|||
assert response.choices[0].message.content == "primary-resp"
|
||||
|
||||
|
||||
def test_process_response_headers_preserves_x_litellm_headers():
|
||||
def test_process_response_headers_preserves_x_litellm_headers_when_internal():
|
||||
"""
|
||||
`process_response_headers` must not add the `llm_provider-` prefix to
|
||||
LiteLLM's own internal headers (anything starting with `x-litellm-`).
|
||||
These are markers set by LiteLLM (e.g. fallback / retry headers); the
|
||||
proxy and other callers look up the bare key.
|
||||
LiteLLM's own internal headers (anything starting with `x-litellm-`) when
|
||||
the caller has marked the input as LiteLLM-owned. These are markers set by
|
||||
LiteLLM (e.g. fallback / retry headers); the proxy and other callers look
|
||||
up the bare key.
|
||||
"""
|
||||
result = process_response_headers(
|
||||
{
|
||||
"x-litellm-attempted-fallbacks": 1,
|
||||
"x-litellm-model-group": "gpt-4",
|
||||
"x-stainless-arch": "arm64",
|
||||
}
|
||||
},
|
||||
preserve_litellm_internal_headers=True,
|
||||
)
|
||||
assert result["x-litellm-attempted-fallbacks"] == 1
|
||||
assert result["x-litellm-model-group"] == "gpt-4"
|
||||
assert result["llm_provider-x-stainless-arch"] == "arm64"
|
||||
|
||||
|
||||
def test_process_response_headers_prefixes_x_litellm_from_raw_provider():
|
||||
"""
|
||||
On raw upstream-provider headers (default `preserve_litellm_internal_headers=False`),
|
||||
a header whose name starts with `x-litellm-` MUST still get the
|
||||
`llm_provider-` prefix. Otherwise a malicious provider could return
|
||||
`x-litellm-attempted-fallbacks` and spoof a LiteLLM-internal marker,
|
||||
bypassing the proxy model-override guard.
|
||||
"""
|
||||
result = process_response_headers(
|
||||
{
|
||||
"x-litellm-attempted-fallbacks": 99,
|
||||
"x-stainless-arch": "arm64",
|
||||
}
|
||||
)
|
||||
assert "x-litellm-attempted-fallbacks" not in result
|
||||
assert result["llm_provider-x-litellm-attempted-fallbacks"] == 99
|
||||
assert result["llm_provider-x-stainless-arch"] == "arm64"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue