diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 38fbe55d85a..741128b2769 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -264,8 +264,8 @@ class Litellm_EntityType(enum.Enum): def hash_token(token: str): import hashlib - # Hash the string using SHA-256 - hashed_token: Final = hashlib.sha256(token.encode()).hexdigest() + # This digest is an opaque lookup identifier, not a password hash. + hashed_token: Final = hashlib.sha256(token.encode(), usedforsecurity=False).hexdigest() return hashed_token diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index 93a37f4a641..d82d69edf2a 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -6024,7 +6024,9 @@ def is_model_allowed_by_pattern(model: str, allowed_model_pattern: str) -> bool: bool: True if model matches the pattern, False otherwise """ if "*" in allowed_model_pattern: - pattern: Final = f"^{allowed_model_pattern.replace('*', '.*')}$" + # Treat the configured model pattern as a glob; only '*' is special. + escaped_pattern: Final = re.escape(allowed_model_pattern) + pattern: Final = "^" + escaped_pattern.replace("\\*", ".*") + "$" return bool(re.match(pattern, model)) return False diff --git a/litellm/proxy/realtime_endpoints/call_sessions.py b/litellm/proxy/realtime_endpoints/call_sessions.py index d1908919503..cf4937f3252 100644 --- a/litellm/proxy/realtime_endpoints/call_sessions.py +++ b/litellm/proxy/realtime_endpoints/call_sessions.py @@ -218,7 +218,10 @@ def decode_call(token: str, authorization: str) -> CodexRealtimeCall: call: Final = CodexRealtimeCall.model_validate_json(plaintext or "") except (ValueError, TypeError, UnicodeError) as exc: raise HTTPException(403, "Invalid realtime call") from exc - if call.expires_at < time.time() or call.owner != hashlib.sha256(authorization.encode()).hexdigest(): + if ( + call.expires_at < time.time() + or call.owner != hashlib.sha256(authorization.encode(), usedforsecurity=False).hexdigest() + ): raise HTTPException(403, "Invalid or expired realtime call") return call @@ -231,6 +234,7 @@ async def _cache_bounded_offer_body(request: Request) -> None: if int(request.headers.get("content-length", "")) > MAX_REALTIME_OFFER_BYTES: raise HTTPException(413, "Realtime offer exceeds the 8 MiB limit") except ValueError: + # A missing or non-numeric content length is checked while streaming below. pass if hasattr(request, "_body"): if len(request._body) > MAX_REALTIME_OFFER_BYTES: # pyright: ignore[reportPrivateUsage] # validate Starlette's cached body without consuming it again @@ -396,7 +400,9 @@ async def _create_codex_realtime_call(request: Request) -> Response: call: Final = parse_call_response( response, alias=model, - owner=hashlib.sha256(f"Bearer {owner_key}".encode()).hexdigest(), + owner=hashlib.sha256( + f"Bearer {owner_key}".encode(), usedforsecurity=False + ).hexdigest(), expires_at=time.time() + 3600, ) except ValueError as exc: diff --git a/litellm/proxy/realtime_endpoints/live.py b/litellm/proxy/realtime_endpoints/live.py index ec9d3db173e..8f68e870497 100644 --- a/litellm/proxy/realtime_endpoints/live.py +++ b/litellm/proxy/realtime_endpoints/live.py @@ -185,7 +185,7 @@ def rewrite_session_ids(value: JsonValue | Mapping[str, JsonValue], raw_id: str, def _owner(auth: UserAPIKeyAuth) -> str: if not auth.api_key: raise HTTPException(403, "Live sessions require an authenticated API key") - return hashlib.sha256(auth.api_key.encode()).hexdigest() + return hashlib.sha256(auth.api_key.encode(), usedforsecurity=False).hexdigest() async def _auth(request: Request) -> UserAPIKeyAuth: @@ -1475,11 +1475,13 @@ async def websocket_live_session(websocket: WebSocket, session_id: str | None = try: await websocket.close(code=1008, reason="Live session rejected") except RuntimeError: + # The peer may have closed the socket before the rejection response. pass except Exception: try: await websocket.close(code=1011, reason="Live upstream connection failed") except RuntimeError: + # The peer may have closed the socket before the failure response. pass finally: if state.connection is not None: diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 6cfacd02e86..dc0ba6bfbd1 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -4291,7 +4291,7 @@ class PrismaClient: def hash_token(self, token: str): # Hash the string using SHA-256 - hashed_token: Final = hashlib.sha256(token.encode()).hexdigest() + hashed_token: Final = hashlib.sha256(token.encode(), usedforsecurity=False).hexdigest() return hashed_token @@ -6718,7 +6718,7 @@ def hash_token(token: str): import hashlib # Hash the string using SHA-256 - hashed_token: Final = hashlib.sha256(token.encode()).hexdigest() + hashed_token: Final = hashlib.sha256(token.encode(), usedforsecurity=False).hexdigest() return hashed_token diff --git a/tests/proxy_unit_tests/test_auth_checks.py b/tests/proxy_unit_tests/test_auth_checks.py index 2538556d3b5..a5f00b01e0a 100644 --- a/tests/proxy_unit_tests/test_auth_checks.py +++ b/tests/proxy_unit_tests/test_auth_checks.py @@ -547,6 +547,8 @@ async def test_virtual_key_max_budget_check( False, ), # don't match on pattern ("openai/gpt-4o", ["openai/*"], True), # openai wildcard access + ("openai/gpt+4", ["openai/gpt+*"], True), # regex metacharacters stay literal + ("openai/gpttt4", ["openai/gpt+*"], False), # regex metacharacters do not overmatch ("gpt-4", ["gpt-3.5-turbo"], False), # model not in allowed list ("claude-3", [], True), # empty model list (allows all) ],