mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(proxy): redact every credential-named request-body field in stored spend-log requests
Replace the hard-coded AWS key check in the spend-log request-body sanitizer with SensitiveDataMasker's key classification, so Azure, Vertex, watsonx, OCI, GigaChat, Gemini and header credentials are redacted too. Proxy-stamped key identity metadata is kept.
This commit is contained in:
parent
f298c9f696
commit
2b75e89231
2 changed files with 84 additions and 8 deletions
|
|
@ -44,11 +44,11 @@ from litellm.litellm_core_utils.litellm_logging import (
|
|||
)
|
||||
from litellm.litellm_core_utils.ptu_pricing import azure_spillover
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps, strip_null_bytes
|
||||
from litellm.litellm_core_utils.sensitive_data_masker import SensitiveDataMasker
|
||||
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload, SpendLogsRouterMetadata
|
||||
from litellm.proxy.route_llm_request import ProxyModelNotFoundError
|
||||
from litellm.proxy.spend_tracking.spend_log_error_logger import spend_log_error
|
||||
from litellm.proxy.utils import PrismaClient, hash_token
|
||||
from litellm.types.llms.bedrock import AWS_CREDENTIAL_VALUE_PARAM_KEYS
|
||||
from litellm.types.router import DeploymentTypedDict, LiteLLM_Params
|
||||
from litellm.types.utils import (
|
||||
PROMPT_CARRYING_GUARDRAIL_FIELDS,
|
||||
|
|
@ -61,6 +61,7 @@ from litellm.types.utils import (
|
|||
StandardLoggingModelInformation,
|
||||
StandardLoggingPayload,
|
||||
StandardLoggingPayloadErrorInformation,
|
||||
StandardLoggingUserAPIKeyMetadata,
|
||||
StandardLoggingVectorStoreRequest,
|
||||
VectorStoreSearchResponse,
|
||||
)
|
||||
|
|
@ -1084,20 +1085,32 @@ def _get_messages_for_spend_logs_payload(
|
|||
|
||||
|
||||
_SENSITIVE_REQUEST_BODY_KEYS: Final = frozenset({"secret_fields"})
|
||||
_REQUEST_BODY_CREDENTIAL_MASKER: Final = SensitiveDataMasker(extra_sensitive_patterns=frozenset({"apikey"}))
|
||||
_PROXY_KEY_IDENTITY_FIELDS: Final = frozenset(StandardLoggingUserAPIKeyMetadata.__annotations__) | {"user_api_key"}
|
||||
|
||||
|
||||
def _is_request_body_credential(key: str, value: object) -> bool:
|
||||
return (
|
||||
isinstance(value, str)
|
||||
and key not in _PROXY_KEY_IDENTITY_FIELDS
|
||||
and _REQUEST_BODY_CREDENTIAL_MASKER.is_sensitive_key(key)
|
||||
)
|
||||
|
||||
|
||||
def _sanitize_request_body_for_spend_logs_payload(
|
||||
request_body: Mapping[str, object],
|
||||
visited: set | None = None,
|
||||
max_string_length_prompt_in_db: int | None = None,
|
||||
redact_credentials: bool = False,
|
||||
) -> dict:
|
||||
"""
|
||||
Recursively sanitize request body to prevent logging large base64 strings or other large values.
|
||||
Truncates strings longer than MAX_STRING_LENGTH_PROMPT_IN_DB characters and handles nested dictionaries.
|
||||
|
||||
At every nesting level, also strips keys listed in _SENSITIVE_REQUEST_BODY_KEYS (e.g. secret_fields,
|
||||
which holds raw HTTP headers including Authorization tokens) and masks string values of AWS
|
||||
credential keys with REDACTED_BY_LITELM_STRING.
|
||||
which holds raw HTTP headers including Authorization tokens). With ``redact_credentials``, string
|
||||
values under keys SensitiveDataMasker classifies as credentials are replaced with
|
||||
REDACTED_BY_LITELM_STRING.
|
||||
"""
|
||||
from litellm.constants import (
|
||||
LITELLM_TRUNCATED_PAYLOAD_FIELD,
|
||||
|
|
@ -1117,7 +1130,9 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
|
||||
def _sanitize_value(value: object) -> object:
|
||||
if isinstance(value, Mapping):
|
||||
return _sanitize_request_body_for_spend_logs_payload(value, visited, max_string_length_prompt_in_db)
|
||||
return _sanitize_request_body_for_spend_logs_payload(
|
||||
value, visited, max_string_length_prompt_in_db, redact_credentials
|
||||
)
|
||||
elif isinstance(value, list):
|
||||
return [_sanitize_value(item) for item in value]
|
||||
elif isinstance(value, str):
|
||||
|
|
@ -1155,9 +1170,7 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
return value
|
||||
|
||||
return {
|
||||
k: REDACTED_BY_LITELM_STRING
|
||||
if k in AWS_CREDENTIAL_VALUE_PARAM_KEYS and isinstance(v, str)
|
||||
else _sanitize_value(v)
|
||||
k: REDACTED_BY_LITELM_STRING if redact_credentials and _is_request_body_credential(k, v) else _sanitize_value(v)
|
||||
for k, v in request_body.items()
|
||||
if k not in _SENSITIVE_REQUEST_BODY_KEYS
|
||||
}
|
||||
|
|
@ -1575,7 +1588,7 @@ def _get_proxy_server_request_for_spend_logs_payload(
|
|||
_request_body = _convert_mapping_to_json_serializable(without_classifier_audit(_request_body))
|
||||
perform_redaction(model_call_details=_request_body, result=None)
|
||||
|
||||
_request_body = _sanitize_request_body_for_spend_logs_payload(_request_body)
|
||||
_request_body = _sanitize_request_body_for_spend_logs_payload(_request_body, redact_credentials=True)
|
||||
_request_body_json_str: Final = safe_dumps(_request_body)
|
||||
if LITELLM_TRUNCATED_PAYLOAD_FIELD in _request_body_json_str:
|
||||
verbose_proxy_logger.info(
|
||||
|
|
|
|||
|
|
@ -2727,6 +2727,69 @@ def test_proxy_server_request_payload_strips_nested_aws_credentials(mock_should_
|
|||
assert parsed["messages"] == [{"role": "user", "content": "hello"}]
|
||||
|
||||
|
||||
@patch("litellm.proxy.spend_tracking.spend_tracking_utils.should_store_prompts_and_responses_in_spend_logs")
|
||||
def test_proxy_server_request_payload_redacts_provider_credentials(mock_should_store: MagicMock) -> None:
|
||||
mock_should_store.return_value = True
|
||||
credentials: Final = {
|
||||
"azure_password": "canary-azure-password",
|
||||
"client_secret": "canary-client-secret",
|
||||
"azure_ad_token": "canary-azure-ad-token",
|
||||
"vertex_credentials": "canary-vertex-credentials",
|
||||
"s3_secret_access_key": "canary-s3-secret",
|
||||
"token": "canary-watsonx-token",
|
||||
"apikey": "canary-watsonx-apikey",
|
||||
"zen_api_key": "canary-zen-api-key",
|
||||
"gemini_api_key": "canary-gemini-api-key",
|
||||
"gigachat_access_token": "canary-gigachat-token",
|
||||
"oci_key": "canary-oci-key",
|
||||
}
|
||||
identity_metadata: Final = {
|
||||
"user_api_key": "hashed-key",
|
||||
"user_api_key_alias": "team-a-key",
|
||||
"user_api_key_team_id": "team-a",
|
||||
"user_api_key_user_id": "user-a",
|
||||
}
|
||||
tool_parameters: Final = {"type": "object", "properties": {"client_secret": {"type": "string"}}}
|
||||
litellm_params: Final = {
|
||||
"proxy_server_request": {
|
||||
"body": {
|
||||
"model": "azure-gpt",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"max_tokens": 10,
|
||||
"vertex_credentials": {"private_key": "canary-private-key", "client_email": "sa@example.com"},
|
||||
"extra_headers": {"Authorization": "Bearer canary-extra-header"},
|
||||
"tools": [
|
||||
{"type": "function", "function": {"name": "f", "parameters": tool_parameters}},
|
||||
{"type": "mcp", "server_url": "https://mcp.example.com", "headers": {"Authorization": "canary-mcp"}},
|
||||
],
|
||||
"fallbacks": [{"model": "azure-b", **credentials}],
|
||||
"metadata": identity_metadata,
|
||||
**credentials,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parsed: Final = json.loads(
|
||||
_get_proxy_server_request_for_spend_logs_payload(metadata={}, litellm_params=litellm_params, kwargs={})
|
||||
)
|
||||
|
||||
assert "canary" not in json.dumps(parsed)
|
||||
assert {name: parsed[name] for name in credentials} == dict.fromkeys(credentials, REDACTED_BY_LITELM_STRING)
|
||||
assert parsed["vertex_credentials"] == REDACTED_BY_LITELM_STRING
|
||||
assert parsed["extra_headers"] == {"Authorization": REDACTED_BY_LITELM_STRING}
|
||||
assert parsed["tools"][0]["function"]["parameters"] == tool_parameters
|
||||
assert parsed["tools"][1]["server_url"] == "https://mcp.example.com"
|
||||
assert parsed["metadata"] == identity_metadata
|
||||
assert parsed["max_tokens"] == 10
|
||||
assert parsed["messages"] == [{"role": "user", "content": "hello"}]
|
||||
|
||||
|
||||
def test_sanitize_request_body_keeps_credential_named_fields_by_default() -> None:
|
||||
response: Final = {"system_fingerprint": "fp_123", "usage": {"prompt_tokens": 1}}
|
||||
|
||||
assert _sanitize_request_body_for_spend_logs_payload({"response": response}) == {"response": response}
|
||||
|
||||
|
||||
@patch("litellm.proxy.spend_tracking.spend_tracking_utils.should_store_prompts_and_responses_in_spend_logs")
|
||||
def test_proxy_server_request_payload_excludes_secret_fields(mock_should_store):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue