fix(passthrough): strip virtual key from all headers on credential-less Vertex forward

The credential-less Vertex passthrough dropped the caller's LiteLLM
virtual key only from Authorization by exact match. A caller who sent
the same key in x-goog-api-key (which doubles as a real Google
credential) had it accepted as a credential and forwarded upstream.

Drop the virtual key by value across every forwarded header, normalizing
any Bearer prefix, so no header name carries it to Google.
This commit is contained in:
mateo-berri 2026-08-24 11:51:23 -07:00
parent e6eb6a4a4d
commit 4bc097733f
2 changed files with 29 additions and 7 deletions

View file

@ -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:

View file

@ -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(