diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 0650735686f..eaed5185fa8 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -1735,26 +1735,35 @@ _CREDENTIALLESS_VERTEX_MISSING_CREDENTIAL_DETAIL: Final = ( ) +def _bearer_stripped(value: str) -> str: + parts: Final = value.split(None, 1) + if len(parts) == 2 and parts[0].lower() == "bearer": + return parts[1] + return value + + def _forwarded_headers_for_credentialless_vertex_passthrough(request: Request) -> Mapping[str, str]: """ Header set to forward on the bring-your-own-credentials Vertex passthrough branch, used when the proxy has no Vertex credential configured. The LiteLLM virtual key that authenticated the caller is never forwarded to - Google: whichever header carried it (``x-litellm-api-key``, or ``Authorization`` - when that is what ``get_litellm_virtual_key`` consumed) is dropped. A caller may - still bring their own Google credential in the ``Authorization`` (OAuth token) or - ``x-goog-api-key`` header; when neither is present the request is rejected so the - virtual key cannot leak upstream. + Google. LiteLLM accepts that key from several headers (``Authorization``, + ``x-litellm-api-key``, ``x-goog-api-key``, ``api-key``, ``x-api-key``), and + ``x-goog-api-key`` doubles as a genuine Google credential, so the key is dropped + by value across every header rather than by name. A caller may still bring their + own Google credential in the ``Authorization`` (OAuth token) or ``x-goog-api-key`` + header; when neither survives the request is rejected so the virtual key cannot + leak upstream. """ incoming: Final = _safe_get_request_headers(request) - litellm_virtual_key: Final = get_litellm_virtual_key(request) + caller_virtual_key: Final = _bearer_stripped(get_litellm_virtual_key(request)) forwarded: Final = MappingProxyType( { name: value for name, value in incoming.items() if name not in ("content-length", "host", "x-litellm-api-key") - and not (name == "authorization" and value == litellm_virtual_key) + and not (caller_virtual_key and _bearer_stripped(value) == caller_virtual_key) } ) if "authorization" not in forwarded and "x-goog-api-key" not in forwarded: 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 9581068f9fc..fe0d1a65c70 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 @@ -3534,6 +3534,19 @@ class TestVertexCredentiallessPassthroughVirtualKeyLeak: assert forwarded is None, "credential-less request must never reach the upstream forwarder" assert raised is not None and raised.status_code == 401 + @pytest.mark.asyncio + async def test_x_goog_api_key_carrying_virtual_key_is_rejected_not_forwarded(self, monkeypatch): + raised, forwarded = await self._run( + monkeypatch, + [ + (b"x-litellm-api-key", self.VKEY.encode()), + (b"x-goog-api-key", self.VKEY.encode()), + (b"content-type", b"application/json"), + ], + ) + assert forwarded is None, "the virtual key in x-goog-api-key must not satisfy the gate nor be forwarded" + assert raised is not None and raised.status_code == 401 + @pytest.mark.asyncio async def test_byo_google_oauth_token_still_forwards_without_virtual_key(self, monkeypatch): raised, forwarded = await self._run(