Merge pull request #20796 from BerriAI/litellm_guardrail_list_sec

[Fix] /v2/guardrails/list Returns Sensitive Values
This commit is contained in:
yuneng-jiang
2026-02-10 16:31:16 -08:00
committed by GitHub
2 changed files with 149 additions and 3 deletions
@@ -15,6 +15,7 @@ from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
from litellm.proxy.guardrails.guardrail_registry import GuardrailRegistry
from litellm.types.guardrails import (
BaseLitellmParams,
PII_ENTITY_CATEGORIES_MAP,
ApplyGuardrailRequest,
ApplyGuardrailResponse,
@@ -150,6 +151,7 @@ async def list_guardrails_v2():
}
```
"""
from litellm.litellm_core_utils.litellm_logging import _get_masked_values
from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER
from litellm.proxy.proxy_server import prisma_client
@@ -164,11 +166,29 @@ async def list_guardrails_v2():
guardrail_configs: List[GuardrailInfoResponse] = []
seen_guardrail_ids = set()
for guardrail in guardrails:
litellm_params: Optional[Union[LitellmParams, dict]] = guardrail.get(
"litellm_params"
)
litellm_params_dict = (
litellm_params.model_dump(exclude_none=True)
if isinstance(litellm_params, LitellmParams)
else litellm_params
) or {}
masked_litellm_params_dict = _get_masked_values(
litellm_params_dict,
unmasked_length=4,
number_of_asterisks=4,
)
masked_litellm_params = (
BaseLitellmParams(**masked_litellm_params_dict)
if masked_litellm_params_dict
else None
)
guardrail_configs.append(
GuardrailInfoResponse(
guardrail_id=guardrail.get("guardrail_id"),
guardrail_name=guardrail.get("guardrail_name"),
litellm_params=guardrail.get("litellm_params"),
litellm_params=masked_litellm_params,
guardrail_info=guardrail.get("guardrail_info"),
created_at=guardrail.get("created_at"),
updated_at=guardrail.get("updated_at"),
@@ -182,11 +202,27 @@ async def list_guardrails_v2():
for guardrail in in_memory_guardrails:
# only add guardrails that are not in DB guardrail list already
if guardrail.get("guardrail_id") not in seen_guardrail_ids:
in_memory_litellm_params_raw = guardrail.get("litellm_params")
in_memory_litellm_params_dict = (
in_memory_litellm_params_raw.model_dump(exclude_none=True)
if isinstance(in_memory_litellm_params_raw, LitellmParams)
else in_memory_litellm_params_raw
) or {}
masked_in_memory_litellm_params = _get_masked_values(
in_memory_litellm_params_dict,
unmasked_length=4,
number_of_asterisks=4,
)
masked_in_memory_litellm_params_typed = (
BaseLitellmParams(**masked_in_memory_litellm_params)
if masked_in_memory_litellm_params
else None
)
guardrail_configs.append(
GuardrailInfoResponse(
guardrail_id=guardrail.get("guardrail_id"),
guardrail_name=guardrail.get("guardrail_name"),
litellm_params=dict(guardrail.get("litellm_params") or {}),
litellm_params=masked_in_memory_litellm_params_typed,
guardrail_info=dict(guardrail.get("guardrail_info") or {}),
guardrail_definition_location="config",
)
@@ -666,11 +702,16 @@ async def get_guardrail_info(guardrail_id: str):
unmasked_length=4,
number_of_asterisks=4,
)
masked_litellm_params = (
BaseLitellmParams(**masked_litellm_params_dict)
if masked_litellm_params_dict
else None
)
return GuardrailInfoResponse(
guardrail_id=result.get("guardrail_id"),
guardrail_name=result.get("guardrail_name"),
litellm_params=masked_litellm_params_dict,
litellm_params=masked_litellm_params,
guardrail_info=dict(result.get("guardrail_info") or {}),
created_at=result.get("created_at"),
updated_at=result.get("updated_at"),
@@ -149,6 +149,111 @@ async def test_list_guardrails_v2_with_db_and_config(
assert isinstance(config_guardrail.litellm_params, BaseLitellmParams)
@pytest.mark.asyncio
async def test_list_guardrails_v2_masks_sensitive_data_in_db_guardrails(mocker):
"""Test that sensitive litellm_params are masked for DB guardrails in list response"""
db_guardrail_with_secrets = {
"guardrail_id": "secret-db-guardrail",
"guardrail_name": "DB Guardrail with Secrets",
"litellm_params": {
"guardrail": "azure/text_moderations",
"mode": "pre_call",
"api_key": "sk-1234567890abcdef",
"api_base": "https://api.secret.example.com",
},
"guardrail_info": {"description": "Test guardrail"},
"created_at": datetime.now(),
"updated_at": datetime.now(),
}
mock_prisma_client = mocker.Mock()
mock_prisma_client.db = mocker.Mock()
mock_prisma_client.db.litellm_guardrailstable = mocker.Mock()
mock_prisma_client.db.litellm_guardrailstable.find_many = AsyncMock(
return_value=[db_guardrail_with_secrets]
)
mock_in_memory_handler = mocker.Mock()
mock_in_memory_handler.list_in_memory_guardrails.return_value = []
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch(
"litellm.proxy.guardrails.guardrail_registry.IN_MEMORY_GUARDRAIL_HANDLER",
mock_in_memory_handler,
)
response = await list_guardrails_v2()
assert len(response.guardrails) == 1
guardrail = response.guardrails[0]
litellm_params = guardrail.litellm_params
if isinstance(litellm_params, dict):
params = litellm_params
else:
params = litellm_params.model_dump() if hasattr(litellm_params, "model_dump") else dict(litellm_params)
# Sensitive keys (containing "key", "secret", "token", etc.) should be masked
assert params["api_key"] != "sk-1234567890abcdef"
assert "****" in str(params["api_key"])
# Non-sensitive keys should remain unchanged
assert params["guardrail"] == "azure/text_moderations"
assert params["mode"] == "pre_call"
assert params["api_base"] == "https://api.secret.example.com"
@pytest.mark.asyncio
async def test_list_guardrails_v2_masks_sensitive_data_in_config_guardrails(mocker):
"""Test that sensitive litellm_params are masked for in-memory/config guardrails in list response"""
config_guardrail_with_secrets = {
"guardrail_id": "secret-config-guardrail",
"guardrail_name": "Config Guardrail with Secrets",
"litellm_params": {
"guardrail": "bedrock",
"mode": "during_call",
"api_key": "my-secret-bedrock-key",
"vertex_credentials": "{sensitive_creds}",
},
"guardrail_info": {"description": "Test guardrail from config"},
}
mock_prisma_client = mocker.Mock()
mock_prisma_client.db = mocker.Mock()
mock_prisma_client.db.litellm_guardrailstable = mocker.Mock()
mock_prisma_client.db.litellm_guardrailstable.find_many = AsyncMock(
return_value=[]
)
mock_in_memory_handler = mocker.Mock()
mock_in_memory_handler.list_in_memory_guardrails.return_value = [
config_guardrail_with_secrets
]
mocker.patch("litellm.proxy.proxy_server.prisma_client", mock_prisma_client)
mocker.patch(
"litellm.proxy.guardrails.guardrail_registry.IN_MEMORY_GUARDRAIL_HANDLER",
mock_in_memory_handler,
)
response = await list_guardrails_v2()
assert len(response.guardrails) == 1
guardrail = response.guardrails[0]
litellm_params = guardrail.litellm_params
if isinstance(litellm_params, dict):
params = litellm_params
else:
params = litellm_params.model_dump() if hasattr(litellm_params, "model_dump") else dict(litellm_params)
# Sensitive keys should be masked
assert params["api_key"] != "my-secret-bedrock-key"
assert "****" in str(params["api_key"])
assert params["vertex_credentials"] != "{sensitive_creds}"
assert "****" in str(params["vertex_credentials"])
# Non-sensitive keys should remain unchanged
assert params["guardrail"] == "bedrock"
assert params["mode"] == "during_call"
@pytest.mark.asyncio
async def test_get_guardrail_info_from_db(mocker, mock_prisma_client):
"""Test getting guardrail info from DB"""