From ab93636e2c5a16c4e6028151f4b6faa17a4036bd Mon Sep 17 00:00:00 2001 From: mateo-berri <277851410+mateo-berri@users.noreply.github.com> Date: Mon, 24 Aug 2026 12:45:41 -0700 Subject: [PATCH] fix(vertex-passthrough): derive credential-header drop set from SpecialHeaders The hand-rolled drop set missed Ocp-Apim-Subscription-Key, so a caller Azure APIM secret in that header was forwarded to Google on the credential-less branch. Derive the name-drop set from the canonical SpecialHeaders.litellm_credential_header_names(), minus Authorization and x-goog-api-key which double as real Google credentials and are value-stripped instead. New credential headers added there are now dropped automatically. --- .../llm_passthrough_endpoints.py | 14 +++++++---- .../test_llm_pass_through_endpoints.py | 24 ++++++++++++++++++- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 99104040831..bcdc9b0a68f 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -1742,8 +1742,9 @@ def _bearer_stripped(value: str) -> str: return value -_HEADERS_NEVER_FORWARDED_TO_VERTEX: Final = frozenset( - {"content-length", "host", "x-litellm-api-key", "api-key", "x-api-key"} +_VERTEX_UPSTREAM_CREDENTIAL_HEADERS: Final = frozenset({"authorization", "x-goog-api-key"}) +_HEADERS_NEVER_FORWARDED_TO_VERTEX: Final = frozenset({"content-length", "host"}) | ( + SpecialHeaders.litellm_credential_header_names() - _VERTEX_UPSTREAM_CREDENTIAL_HEADERS ) @@ -1772,9 +1773,12 @@ def _forwarded_headers_for_credentialless_vertex_passthrough(request: Request) - branch, used when the proxy has no Vertex credential configured. No credential the proxy accepts for caller authentication is forwarded to - Google. Vertex only ever authenticates with an OAuth token in ``Authorization`` - or an API key in ``x-goog-api-key``, so the proxy-only auth headers Google never - consumes (``x-litellm-api-key`` / ``api-key`` / ``x-api-key``) are dropped by + Google. ``user_api_key_auth`` reads the caller's key from every header in + ``SpecialHeaders.litellm_credential_header_names()``, and Vertex only ever + authenticates with an OAuth token in ``Authorization`` or an API key in + ``x-goog-api-key``. So the proxy-only auth headers Google never consumes + (everything in that set except those two, e.g. ``x-litellm-api-key`` / + ``api-key`` / ``x-api-key`` / ``Ocp-Apim-Subscription-Key``) are dropped by name. ``Authorization`` and ``x-goog-api-key`` may instead carry a genuine bring-your-own Google credential, so they are kept unless their value is one of the caller's LiteLLM key values, which are dropped by value (normalizing any diff --git a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index c5e56788a96..b65e2b2f499 100644 --- a/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/test_litellm/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -35,7 +35,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( vertex_proxy_route, vllm_proxy_route, ) -from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth +from litellm.proxy._types import LitellmUserRoles, SpecialHeaders, UserAPIKeyAuth from litellm.types.passthrough_endpoints.vertex_ai import VertexPassThroughCredentials @@ -3608,6 +3608,28 @@ class TestVertexCredentiallessPassthroughVirtualKeyLeak: assert "azure-style-caller-secret" not in forwarded_blob assert "anthropic-style-caller-secret" not in forwarded_blob + @pytest.mark.asyncio + @pytest.mark.parametrize( + "credential_header", + sorted(SpecialHeaders.litellm_credential_header_names() - {"authorization", "x-goog-api-key"}), + ) + async def test_every_non_google_credential_header_is_dropped_by_name(self, monkeypatch, credential_header): + raised, forwarded = await self._run( + monkeypatch, + [ + (b"x-goog-api-key", b"AIza-real-google-api-key"), + (credential_header.encode(), b"some-distinct-caller-secret-value"), + (b"content-type", b"application/json"), + ], + ) + assert raised is None + assert forwarded is not None + assert forwarded.get("x-goog-api-key") == "AIza-real-google-api-key" + assert credential_header not in forwarded + assert "some-distinct-caller-secret-value" not in " ".join( + f"{name}:{value}" for name, value in forwarded.items() + ) + @pytest.mark.asyncio async def test_virtual_key_in_operator_configured_header_is_stripped(self, monkeypatch): with mock.patch.dict( # test-quality-ok: general_settings is the real proxy config surface for litellm_key_header_name; no injection seam exists on this route