mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge 19a65aaebe into 98acc01e0d
This commit is contained in:
commit
abc1fc19e1
5 changed files with 111 additions and 1 deletions
|
|
@ -25,6 +25,21 @@ def validate_finite_spend(spend: float | None) -> None:
|
|||
)
|
||||
|
||||
|
||||
def validate_no_null_bytes(params: Mapping[str, str | None]) -> None:
|
||||
"""Reject null bytes in string params before they reach the DB.
|
||||
|
||||
Postgres rejects strings containing ``\\x00`` outright, and the raw
|
||||
driver error used to reach management endpoint clients in a 500 body,
|
||||
leaking database internals (issue #46076).
|
||||
"""
|
||||
for name, value in params.items():
|
||||
if value is not None and "\x00" in value:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail={"error": f"{name} must not contain null bytes"},
|
||||
)
|
||||
|
||||
|
||||
def validate_budget_duration(budget_duration: str | None, status_code: int = 400) -> None:
|
||||
"""Reject budget durations that can't be parsed, are non-positive, or
|
||||
overflow date math, so a bad value can't be persisted and later crash the
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@ from litellm.proxy.management_endpoints.common_utils import ( # noqa: F401 # l
|
|||
user_api_key_has_admin_view,
|
||||
validate_budget_duration,
|
||||
validate_finite_spend,
|
||||
validate_no_null_bytes,
|
||||
)
|
||||
from litellm.proxy.management_endpoints.key_budget_change import (
|
||||
KeyBudgetAdminOnly,
|
||||
|
|
@ -4973,6 +4974,8 @@ async def info_key_fn_v2(
|
|||
)
|
||||
# Resolve key_aliases to tokens so we never pass token=None (unbounded query)
|
||||
tokens_to_query: Final = list(data.keys) if data.keys else []
|
||||
for token in tokens_to_query:
|
||||
validate_no_null_bytes({"keys": token})
|
||||
if data.key_aliases:
|
||||
alias_rows: Final = await _prisma_table(VerificationTokenRepository(prisma_client)).find_many(
|
||||
where={"key_alias": {"in": data.key_aliases}},
|
||||
|
|
@ -5096,6 +5099,8 @@ async def info_key_fn(
|
|||
prisma_client,
|
||||
)
|
||||
|
||||
validate_no_null_bytes({"key": key})
|
||||
|
||||
try:
|
||||
if prisma_client is None:
|
||||
raise Exception(
|
||||
|
|
|
|||
|
|
@ -3463,8 +3463,19 @@ async def view_spend_logs(
|
|||
-H "Authorization: Bearer $LITELLM_MASTER_KEY"
|
||||
```
|
||||
"""
|
||||
from litellm.proxy.management_endpoints.common_utils import validate_no_null_bytes
|
||||
from litellm.proxy.proxy_server import prisma_client
|
||||
|
||||
validate_no_null_bytes(
|
||||
{
|
||||
"api_key": api_key,
|
||||
"user_id": user_id,
|
||||
"request_id": request_id,
|
||||
"start_date": start_date,
|
||||
"end_date": end_date,
|
||||
}
|
||||
)
|
||||
|
||||
if (
|
||||
user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER
|
||||
or user_api_key_dict.user_role == LitellmUserRoles.INTERNAL_USER_VIEW_ONLY
|
||||
|
|
@ -3587,8 +3598,9 @@ async def view_spend_logs(
|
|||
)
|
||||
elif isinstance(e, ProxyException):
|
||||
raise e
|
||||
verbose_proxy_logger.exception("Unhandled error in /spend/logs")
|
||||
raise ProxyException(
|
||||
message="/spend/logs Error" + str(e),
|
||||
message="Internal server error from /spend/logs. Details are in the proxy server logs.",
|
||||
type="internal_error",
|
||||
param=openai_error_param(e),
|
||||
code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
|
|
|
|||
|
|
@ -23964,6 +23964,27 @@ async def test_key_generate_reads_the_team_from_the_writer_database_not_the_cach
|
|||
assert raised.value.message == "Team not found for team_id=1234. Non-admin users cannot create keys for non-existent teams."
|
||||
|
||||
|
||||
def test_key_info_null_byte_key_rejected():
|
||||
"""Regression test for https://github.com/BerriAI/litellm/issues/46076
|
||||
|
||||
A null byte in the key query param used to reach Prisma, whose raw
|
||||
driver error came back to the caller in the 500 body. It must be
|
||||
rejected up front with a 400 that carries no database internals.
|
||||
"""
|
||||
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
||||
|
||||
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-test", user_id="1234"
|
||||
)
|
||||
try:
|
||||
response = client.get("/key/info", params={"key": "test\x00"})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "null bytes" in response.text
|
||||
finally:
|
||||
app.dependency_overrides.pop(user_api_key_auth, None)
|
||||
|
||||
|
||||
def test_key_info_unexpected_database_exception_sanitized(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
|
|
|||
|
|
@ -8350,3 +8350,60 @@ def test_spend_calculate_rejection_body_is_openai_shaped(client):
|
|||
assert error["type"] == "invalid_request_error"
|
||||
assert error["param"] is None
|
||||
assert error["code"] == "400"
|
||||
|
||||
|
||||
def test_view_spend_logs_null_byte_api_key_rejected(client):
|
||||
"""Regression test for https://github.com/BerriAI/litellm/issues/46076
|
||||
|
||||
A null byte in the api_key query param used to reach Prisma, whose raw
|
||||
driver error came back to the caller in the 500 body. It must be
|
||||
rejected up front with a 400 that carries no database internals.
|
||||
"""
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
try:
|
||||
response = client.get("/spend/logs", params={"api_key": "test\x00"})
|
||||
|
||||
assert response.status_code == 400
|
||||
assert "null bytes" in response.text
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
||||
|
||||
def test_view_spend_logs_db_error_returns_generic_message(client, monkeypatch):
|
||||
"""Regression test for https://github.com/BerriAI/litellm/issues/46076
|
||||
|
||||
When the database does fail under /spend/logs, the raw driver error
|
||||
must stay in the server logs and the 500 body must carry a generic
|
||||
message instead of database internals.
|
||||
"""
|
||||
|
||||
class RaisingDB:
|
||||
def __init__(self):
|
||||
self.litellm_spendlogs = self
|
||||
|
||||
async def find_many(self, *args, **kwargs):
|
||||
raise Exception('Postgres rejected "test": invalid byte sequence for encoding "UTF8"')
|
||||
|
||||
async def query_raw(self, *args, **kwargs):
|
||||
raise Exception('Postgres rejected "test": invalid byte sequence for encoding "UTF8"')
|
||||
|
||||
class RaisingPrismaClient:
|
||||
def __init__(self):
|
||||
self.db = RaisingDB()
|
||||
|
||||
monkeypatch.setattr(
|
||||
"litellm.proxy.proxy_server.prisma_client", RaisingPrismaClient()
|
||||
)
|
||||
app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(
|
||||
user_role=LitellmUserRoles.PROXY_ADMIN
|
||||
)
|
||||
try:
|
||||
response = client.get("/spend/logs", params={"api_key": "sk-normal"})
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "Postgres" not in response.text
|
||||
assert "server logs" in response.text
|
||||
finally:
|
||||
app.dependency_overrides.pop(ps.user_api_key_auth, None)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue