fix(proxy): mask default_litellm_params secrets for non-admin callers of /get/config/callbacks

default_litellm_params is merged into every completion call's kwargs, so an
operator can put a shared api_key or an Authorization header under
extra_headers there. Router.get_settings() now returns it (needed so the
Admin UI's Router Settings page can display/edit it), but /get/config/callbacks
forwarded router_settings verbatim regardless of caller role - unlike the
callback and alerting env vars on the same response, which already redact for
non-full-admin callers (e.g. PROXY_ADMIN_VIEW_ONLY). That let a read-only
admin read another admin's upstream provider credentials.

Add the same role gate used for callback/alerting env vars, scoped to
default_litellm_params via the existing SensitiveDataMasker so any
key/secret/token/auth-shaped field is masked for non-full-admin callers while
full admins keep seeing the real value.
This commit is contained in:
Krrish Dholakia 2026-07-13 18:32:51 -07:00
parent 750e849d12
commit 6eff7264c0
2 changed files with 74 additions and 1 deletions

View file

@ -14620,6 +14620,23 @@ def _apply_webhook_role_gate(webhook_map, is_full_admin: bool):
return {alert_type: "REDACTED" for alert_type in webhook_map}
def _apply_router_settings_role_gate(router_settings: dict, is_full_admin: bool) -> dict:
"""
``default_litellm_params`` is an open-ended kwargs dict (Router merges it into
every completion call), so an admin can put a shared ``api_key`` or an
``extra_headers`` Authorization token there. Unlike the other fields on
this page, nothing else masks it before it reaches non-full-admin callers
of /get/config/callbacks.
"""
default_litellm_params = router_settings.get("default_litellm_params")
if is_full_admin or not isinstance(default_litellm_params, dict):
return router_settings
return {
**router_settings,
"default_litellm_params": SENSITIVE_DATA_MASKER.mask_dict(default_litellm_params),
}
@router.get(
"/config/field/info",
tags=["config.yaml"],
@ -15224,7 +15241,7 @@ async def get_config(
if llm_router is None:
_router_settings = {}
else:
_router_settings = llm_router.get_settings()
_router_settings = _apply_router_settings_role_gate(llm_router.get_settings(), is_full_admin)
return {
"status": "success",

View file

@ -924,6 +924,62 @@ def test_get_config_custom_callback_api_env_vars(monkeypatch):
}
def _get_config_with_default_litellm_params(monkeypatch, user_role):
"""
default_litellm_params is merged into every completion call, so an admin
can put a shared api_key/extra_headers Authorization token there. Router
exposes it verbatim via get_settings(); /get/config/callbacks must not
forward that verbatim to callers who aren't full proxy admins.
"""
from litellm.proxy.proxy_server import app, proxy_config, user_api_key_auth
mock_router = MagicMock()
mock_router.get_settings.return_value = {
"default_litellm_params": {
"api_key": "sk-super-secret-upstream-key",
"timeout": 30,
},
}
monkeypatch.setattr("litellm.proxy.proxy_server.llm_router", mock_router)
monkeypatch.setattr(
proxy_config,
"get_config",
AsyncMock(return_value={"litellm_settings": {}, "general_settings": {}, "environment_variables": {}}),
)
original_overrides = app.dependency_overrides.copy()
app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(
user_role=user_role, api_key="sk-1234"
)
client = TestClient(app)
try:
return client.get("/get/config/callbacks")
finally:
app.dependency_overrides = original_overrides
def test_get_config_masks_default_litellm_params_secrets_for_non_admin(monkeypatch):
response = _get_config_with_default_litellm_params(
monkeypatch, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY
)
assert response.status_code == 200
default_litellm_params = response.json()["router_settings"]["default_litellm_params"]
assert "sk-super-secret-upstream-key" not in json.dumps(default_litellm_params)
# non-sensitive keys are unaffected
assert default_litellm_params["timeout"] == 30
def test_get_config_returns_default_litellm_params_unmasked_for_full_admin(monkeypatch):
response = _get_config_with_default_litellm_params(monkeypatch, LitellmUserRoles.PROXY_ADMIN)
assert response.status_code == 200
default_litellm_params = response.json()["router_settings"]["default_litellm_params"]
assert default_litellm_params["api_key"] == "sk-super-secret-upstream-key"
assert default_litellm_params["timeout"] == 30
def test_get_config_returns_email_settings(monkeypatch):
"""
Regression for https://github.com/BerriAI/litellm/issues/19221