mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-10 03:28:53 +00:00
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:
parent
e6eb6a4a4d
commit
4bc097733f
2 changed files with 29 additions and 7 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue