From d22a247fb616d1fb29bf57abb3e488db5b247280 Mon Sep 17 00:00:00 2001 From: Kylinny Date: Thu, 22 Oct 2026 03:52:44 +0000 Subject: [PATCH 1/2] fix(a2a): forward allowlisted x-litellm-* headers on message/send Allowlisted x-litellm-* headers (e.g. x-litellm-api-key) configured via agent extra_headers are now forwarded to the backend A2A agent. Previously _forwarding_headers() dropped every x-litellm-* header, which silently discarded the allowlisted values. The blanket prefix filter is replaced with an exact, case-insensitive deny-list of proxy-minted identity headers (x-litellm-agent-id, x-litellm-user-id, x-litellm-team-id, x-litellm-trace-id), so forged identity headers still cannot pass through or overwrite the proxy-minted values. Fixes BerriAI/litellm#43450 Signed-off-by: Kylinny --- .../proxy/agent_endpoints/a2a_endpoints.py | 22 +++++++++++-- .../agent_endpoints/test_a2a_endpoints.py | 31 +++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py index c11d296837e..eb083556590 100644 --- a/litellm/proxy/agent_endpoints/a2a_endpoints.py +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -46,6 +46,7 @@ from litellm.proxy.common_utils.sse_keepalive import ( wrap_sse_stream_with_keepalive_pings, ) from litellm.proxy.utils import ProxyLogging, get_custom_url +from litellm.types.agents import AGENT_CALLER_TEAM_ID_HEADER, AGENT_CALLER_USER_ID_HEADER from litellm.types.utils import all_litellm_params if TYPE_CHECKING: @@ -180,6 +181,21 @@ async def _resolve_backend_auth( ) +# Headers the proxy mints itself on the backend call. Forged client copies of +# these must not be forwarded (agent-id is minted later in asend_message, where +# agent_extra_headers would otherwise overwrite it), but any other x-litellm-* +# header (e.g. an admin-allowlisted x-litellm-api-key from extra_headers) is +# valid passthrough. +_MINTED_A2A_IDENTITY_HEADERS: Final = frozenset( + { + AGENT_CALLER_USER_ID_HEADER.lower(), + AGENT_CALLER_TEAM_ID_HEADER.lower(), + "x-litellm-trace-id", + "x-litellm-agent-id", + } +) + + def _forwarding_headers( caller_identity: Mapping[str, str], request_data: Mapping[str, object], @@ -187,11 +203,13 @@ def _forwarding_headers( backend_auth_header: Mapping[str, str] | None, ) -> 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) + minted_names: Final = frozenset( + name.lower() for name, _ in backend_auth + ) | _MINTED_A2A_IDENTITY_HEADERS 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 minted_names ) trace_id: Final = request_data.get("litellm_trace_id") trace: Final = (("X-LiteLLM-Trace-Id", str(trace_id)),) if trace_id else () diff --git a/tests/unit/proxy/agent_endpoints/test_a2a_endpoints.py b/tests/unit/proxy/agent_endpoints/test_a2a_endpoints.py index a2448694dd7..482dd464f66 100644 --- a/tests/unit/proxy/agent_endpoints/test_a2a_endpoints.py +++ b/tests/unit/proxy/agent_endpoints/test_a2a_endpoints.py @@ -771,6 +771,37 @@ async def test_message_methods_caller_identity_headers_cannot_be_spoofed(method: ) +@pytest.mark.asyncio +@pytest.mark.parametrize("method", ["message/send", "message/stream"]) +async def test_message_methods_forward_allowlisted_x_litellm_api_key(method: str): + """Issue #43450: an x-litellm-* header the admin allowlisted in extra_headers must be + forwarded to the backend agent, while forged copies of the proxy-minted identity + headers are still stripped.""" + agent = _make_agent_mock() + agent.extra_headers = ["x-litellm-api-key"] + mock_request = _make_request_mock(method, _HELLO_MESSAGE_PARAMS) + mock_request.headers = { + "x-litellm-api-key": "sk-allowlisted", + "x-a2a-test-agent-x-litellm-team-id": "attacker-team", + "x-a2a-test-agent-x-litellm-agent-id": "attacker-agent", + } + user_api_key_dict = UserAPIKeyAuth(api_key="sk-test", user_id="real-user", team_id="real-team") + + captured = await _invoke_message_method(method, mock_request, user_api_key_dict, agent=agent) + + forwarded_headers = captured.agent_extra_headers or {} + assert forwarded_headers.get("x-litellm-api-key") == "sk-allowlisted", ( + "admin-allowlisted x-litellm-api-key must reach the backend agent" + ) + assert forwarded_headers.get("X-LiteLLM-User-Id") == "real-user" + assert forwarded_headers.get("X-LiteLLM-Team-Id") == "real-team", ( + "proxy-minted team id must not be overridden by a forged client header" + ) + assert "x-litellm-agent-id" not in {k.lower() for k in forwarded_headers}, ( + "forged agent id must be stripped before asend_message mints the real one" + ) + + @pytest.mark.asyncio @pytest.mark.parametrize("method", ["message/send", "message/stream"]) async def test_message_methods_forward_key_bound_identity_not_pre_call_rewrite(method: str): From 9fa7e2f60e75af2ccc7ce322fd8cb68a28b48a08 Mon Sep 17 00:00:00 2001 From: Kylinny Date: Thu, 22 Oct 2026 06:37:32 +0000 Subject: [PATCH 2/2] style: ruff format a2a_endpoints.py Fix CI 'Check ruff format' failure. Signed-off-by: Kylinny --- litellm/proxy/agent_endpoints/a2a_endpoints.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/litellm/proxy/agent_endpoints/a2a_endpoints.py b/litellm/proxy/agent_endpoints/a2a_endpoints.py index eb083556590..40c9f4b0ea1 100644 --- a/litellm/proxy/agent_endpoints/a2a_endpoints.py +++ b/litellm/proxy/agent_endpoints/a2a_endpoints.py @@ -203,9 +203,7 @@ def _forwarding_headers( backend_auth_header: Mapping[str, str] | None, ) -> 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 - ) | _MINTED_A2A_IDENTITY_HEADERS + minted_names: Final = frozenset(name.lower() for name, _ in backend_auth) | _MINTED_A2A_IDENTITY_HEADERS passthrough: Final = tuple( (name, value) for name, value in (agent_extra_headers.items() if agent_extra_headers else ())