This commit is contained in:
Baojiang Lee 2026-11-07 01:11:16 +00:00 • committed by GitHub
commit abc1fc19e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 111 additions and 1 deletions

View file

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

View file

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

View file

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

View file

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

View file

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