This commit is contained in:
Gyanu Mayank 2026-09-30 10:30:46 -04:00 • committed by GitHub
commit 906bbce5be
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 1 deletions

View file

@ -172,6 +172,18 @@ async def _resolve_backend_auth_header(
return await resolve_a2a_hop_auth_header(litellm_params, custom_llm_provider)
# Identity / hop headers LiteLLM mints itself. Drop client-supplied copies (any
# casing) so they cannot sit beside the authenticated values. Other x-litellm-*
# names, including an admin-allowlisted x-litellm-api-key, still pass through.
_RESERVED_X_LITELLM_HEADERS: Final = frozenset(
{
"x-litellm-user-id",
"x-litellm-team-id",
"x-litellm-trace-id",
}
)
def _forwarding_headers(
caller_identity: Mapping[str, str],
request_data: Mapping[str, object],
@ -180,10 +192,11 @@ def _forwarding_headers(
) -> dict[str, str] | None:
backend_auth: Final = tuple(backend_auth_header.items()) if backend_auth_header else ()
minted_names: Final = frozenset(name.lower() for name, _ in backend_auth)
blocked: Final = _RESERVED_X_LITELLM_HEADERS | minted_names
passthrough: Final = tuple(
(name, value)
for name, value in (agent_extra_headers.items() if agent_extra_headers else ())
if not name.lower().startswith("x-litellm-") and name.lower() not in minted_names
if name.lower() not in blocked
)
trace_id: Final = request_data.get("litellm_trace_id")
trace: Final = (("X-LiteLLM-Trace-Id", str(trace_id)),) if trace_id else ()

View file

@ -2684,3 +2684,48 @@ def test_forwarding_headers_minted_bearer_replaces_a_forwarded_authorization_of_
)
assert merged == {"X-Custom": "kept", "Authorization": "Bearer minted-token"}
def test_forwarding_headers_keeps_allowlisted_x_litellm_api_key():
"""Admin-configured extra_headers may include x-litellm-api-key. That name is not a minted
caller-identity header, so it must reach the backend agent (#43450)."""
from litellm.proxy.agent_endpoints.a2a_endpoints import _forwarding_headers
merged = _forwarding_headers(
caller_identity={"X-LiteLLM-User-Id": "u1", "X-LiteLLM-Team-Id": "t1"},
request_data={},
agent_extra_headers={
"authorization": "Bearer client",
"x-litellm-api-key": "sk-forward-me",
"x-litellm-user-id": "attacker",
},
backend_auth_header=None,
)
assert merged is not None
assert merged.get("x-litellm-api-key") == "sk-forward-me"
assert merged.get("authorization") == "Bearer client"
assert merged.get("X-LiteLLM-User-Id") == "u1"
assert merged.get("X-LiteLLM-Team-Id") == "t1"
# Spoofed lowercase identity must not sit beside the minted header.
assert "x-litellm-user-id" not in merged
@pytest.mark.asyncio
@pytest.mark.parametrize("method", ["message/send", "message/stream"])
async def test_message_methods_forward_allowlisted_x_litellm_api_key(method: str):
agent = _make_agent_mock()
agent.extra_headers = ["authorization", "x-litellm-api-key"]
mock_request = _make_request_mock(method, _HELLO_MESSAGE_PARAMS)
mock_request.headers = {
"authorization": "Bearer client-key",
"x-litellm-api-key": "sk-allowlisted",
}
user_api_key_dict = UserAPIKeyAuth(api_key="sk-test", user_id="u1", team_id="t1")
captured = await _invoke_message_method(method, mock_request, user_api_key_dict, agent=agent)
forwarded = captured.agent_extra_headers or {}
assert forwarded.get("authorization") == "Bearer client-key"
assert forwarded.get("x-litellm-api-key") == "sk-allowlisted"
assert forwarded.get("X-LiteLLM-Team-Id") == "t1"