fix(realtime): redact credentials from the relayed upstream close

The handshake error path already runs client-facing error strings through
_redact_string; the relay's _close_client did not, so a secret echoed in an
upstream close reason could reach the client verbatim. Mirror the handshake
path and scrub the close message and reason before relaying them.
This commit is contained in:
mateo-berri 2026-09-04 22:52:26 -07:00
parent 412c36bb8e
commit 74613f9bd4
2 changed files with 23 additions and 3 deletions

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any, Final, NoReturn, Protocol, TypedDict, cas
from typing_extensions import ReadOnly
import litellm
from litellm._logging import verbose_logger
from litellm._logging import _redact_string, verbose_logger
from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER
from litellm.llms.base_llm.realtime.transformation import BaseRealtimeConfig
from litellm.types.llms.openai import (
@ -1567,12 +1567,14 @@ class RealTimeStreaming:
await asyncio.gather(forward_task, client_task, return_exceptions=True)
async def _close_client(self, close: BackendClose) -> None:
redacted_message: Final = _redact_string(close.message)
redacted_reason: Final = _redact_string(close.reason)
try:
if close.code != 1000:
await self.websocket.send_text(realtime_error_event(close.message, error_type="server_error"))
await self.websocket.send_text(realtime_error_event(redacted_message, error_type="server_error"))
await self.websocket.close(
code=client_close_code(close.code),
reason=websocket_close_reason(close.reason, fallback=close.message),
reason=websocket_close_reason(redacted_reason, fallback=redacted_message),
)
except Exception as e: # noqa: BLE001 # the client may already be gone; the session is over either way
verbose_logger.debug("Could not relay the upstream close to the client: %s", e)

View file

@ -3229,6 +3229,24 @@ async def test_bidirectional_forward_relays_upstream_policy_close_to_client():
client_ws.close.assert_awaited_once_with(code=1008, reason=_UPSTREAM_REFUSAL)
@pytest.mark.asyncio
async def test_upstream_close_reason_with_a_secret_is_redacted_before_reaching_the_client():
"""LIT-6973: the relayed close mirrors the handshake path and scrubs credential
patterns, so an upstream error echoing a token never reaches the client verbatim."""
secret: Final = "sk-live-abcdef0123456789abcdef0123"
client_ws: Final = _client_ws_that_never_sends()
upstream_close: Final = ConnectionClosed(Close(1008, f"auth failed for {secret}"), None)
session: Final = _relay_session(client_ws, _backend_ws_closing_with(upstream_close))
await session.run()
(error_event,) = _error_events_sent_to(client_ws)
assert secret not in error_event["error"]["message"]
relayed_reason: Final = client_ws.close.await_args.kwargs["reason"]
assert secret not in relayed_reason
assert "REDACTED" in relayed_reason
@pytest.mark.asyncio
async def test_bidirectional_forward_maps_abnormal_upstream_close_to_internal_error():
client_ws: Final = _client_ws_that_never_sends()