fix(proxy): reject out-of-range alerting_args on config field update

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yassin 2026-08-31 16:29:40 +00:00
parent 3bfc77ac18
commit 73d77e719b
3 changed files with 86 additions and 6 deletions

View file

@ -39,7 +39,7 @@ from typing import (
import anyio
import websockets
import websockets.exceptions
from pydantic import BaseModel, Json, JsonValue
from pydantic import BaseModel, Json, JsonValue, ValidationError
from typing_extensions import NotRequired, assert_never
from litellm._uuid import uuid
@ -16172,6 +16172,16 @@ async def update_config_general_settings(
detail={"error": f"Invalid type of field value={type(data.field_value)} passed in."},
)
if data.field_name == "alerting_args":
try:
SlackAlertingArgs.model_validate(data.field_value)
except ValidationError as e:
errors: Final = "; ".join(f"{'.'.join(str(loc) for loc in err['loc'])}: {err['msg']}" for err in e.errors())
raise HTTPException(
status_code=400,
detail={"error": f"Invalid alerting_args: {errors}"},
)
## get general settings from db
db_general_settings: Final = await _config_param_table(prisma_client).find_first(
where={"param_name": "general_settings"}

View file

@ -10319,6 +10319,75 @@ async def test_update_config_general_settings_emits_audit_log(monkeypatch):
assert before["some_api_key"] != "sk-stored-secret"
@pytest.mark.asyncio
async def test_update_config_field_rejects_out_of_range_alerting_args(monkeypatch):
"""Out-of-range alerting_args must be rejected at save time. If they land in the
DB, SlackAlertingArgs raises during the config reload and alerting breaks."""
from unittest.mock import MagicMock
from fastapi import HTTPException
import litellm.proxy.proxy_server as proxy_server_module
from litellm.proxy._types import ConfigFieldUpdate
from litellm.proxy.proxy_server import update_config_general_settings
monkeypatch.setattr(proxy_server_module, "prisma_client", MagicMock())
admin = UserAPIKeyAuth(
api_key="hashed-admin",
user_id="admin-1",
user_role=LitellmUserRoles.PROXY_ADMIN,
)
with pytest.raises(HTTPException) as exc_info:
await update_config_general_settings(
data=ConfigFieldUpdate(
field_name="alerting_args",
field_value={
"daily_spend_per_user_threshold": -5.0,
"user_spend_check_interval": 20,
},
config_type="general_settings",
),
user_api_key_dict=admin,
)
assert exc_info.value.status_code == 400
error_msg = exc_info.value.detail["error"]
assert "daily_spend_per_user_threshold" in error_msg
assert "user_spend_check_interval" in error_msg
@pytest.mark.asyncio
async def test_update_config_field_accepts_valid_alerting_args(monkeypatch):
import litellm.proxy.proxy_server as proxy_server_module
from litellm.proxy._types import ConfigFieldUpdate
from litellm.proxy.proxy_server import update_config_general_settings
fake = _fake_prisma_with_config({})
monkeypatch.setattr(proxy_server_module, "prisma_client", fake)
monkeypatch.setattr(litellm, "store_audit_logs", False)
admin = UserAPIKeyAuth(
api_key="hashed-admin",
user_id="admin-1",
user_role=LitellmUserRoles.PROXY_ADMIN,
)
await update_config_general_settings(
data=ConfigFieldUpdate(
field_name="alerting_args",
field_value={
"daily_spend_per_user_threshold": 5.0,
"user_spend_check_interval": 60,
},
config_type="general_settings",
),
user_api_key_dict=admin,
)
written = json.loads(fake.db.litellm_config.upsert.call_args.kwargs["data"]["update"]["param_value"])
assert written["alerting_args"]["daily_spend_per_user_threshold"] == 5.0
@pytest.mark.asyncio
async def test_update_config_general_settings_applies_ssrf_globals(monkeypatch):
import litellm.proxy.proxy_server as proxy_server_module

View file

@ -5,6 +5,7 @@ import React, { useState, useEffect } from "react";
import { alertingSettingsCall, updateConfigFieldSetting } from "../networking";
import DynamicForm from "./dynamic_form";
import { extractProxyErrorMessage } from "@/lib/http/client";
import { toast } from "@/lib/toast";
interface alertingSettingsItem {
field_name: string;
@ -43,7 +44,7 @@ const AlertingSettings: React.FC<AlertingSettingsProps> = ({ accessToken, premiu
setAlertingSettings(updatedSettings);
};
const handleSubmit = (formValues: Record<string, any>) => {
const handleSubmit = async (formValues: Record<string, any>) => {
if (!accessToken) {
return;
}
@ -64,18 +65,18 @@ const AlertingSettings: React.FC<AlertingSettingsProps> = ({ accessToken, premiu
const mergedFormValues = { ...formValues, ...initialFormValues };
const { slack_alerting, ...alertingArgs } = mergedFormValues;
try {
updateConfigFieldSetting(accessToken, "alerting_args", alertingArgs);
await updateConfigFieldSetting(accessToken, "alerting_args", alertingArgs);
if (typeof slack_alerting === "boolean") {
if (slack_alerting == true) {
updateConfigFieldSetting(accessToken, "alerting", ["slack"]);
await updateConfigFieldSetting(accessToken, "alerting", ["slack"]);
} else {
updateConfigFieldSetting(accessToken, "alerting", []);
await updateConfigFieldSetting(accessToken, "alerting", []);
}
}
// update value in state
toast.success("Wait 10s for proxy to update.");
} catch (error) {
// do something
toast.error(extractProxyErrorMessage(error));
}
};