mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
fix(proxy): keep request-body credentials out of stored spend-log requests (#43635)
* fix(proxy): keep request-body aws credentials out of stored spend-log requests * 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. * fix(proxy): keep request identifiers named like keys in stored spend-log requests * refactor(proxy): drop the AWS-only snapshot exclusion now that spend-log redaction is name-based * refactor(proxy): use SensitiveDataMasker's key classification without an exclusion list * refactor(proxy): always redact credential-named fields in stored spend-log payloads
This commit is contained in:
parent
025292e75b
commit
7204942756
2 changed files with 115 additions and 6 deletions
|
|
@ -44,6 +44,7 @@ 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
|
||||
|
|
@ -1083,6 +1084,11 @@ 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"}))
|
||||
|
||||
|
||||
def _is_request_body_credential(key: str, value: object) -> bool:
|
||||
return isinstance(value, str) and _REQUEST_BODY_CREDENTIAL_MASKER.is_sensitive_key(key)
|
||||
|
||||
|
||||
def _sanitize_request_body_for_spend_logs_payload(
|
||||
|
|
@ -1094,8 +1100,9 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
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.
|
||||
|
||||
Also strips keys listed in _SENSITIVE_REQUEST_BODY_KEYS (e.g. secret_fields
|
||||
which contains raw HTTP headers including Authorization tokens).
|
||||
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 replaces string values under keys
|
||||
SensitiveDataMasker classifies as credentials with REDACTED_BY_LITELM_STRING.
|
||||
"""
|
||||
from litellm.constants import (
|
||||
LITELLM_TRUNCATED_PAYLOAD_FIELD,
|
||||
|
|
@ -1152,7 +1159,11 @@ def _sanitize_request_body_for_spend_logs_payload(
|
|||
return value
|
||||
return value
|
||||
|
||||
return {k: _sanitize_value(v) for k, v in request_body.items() if k not in _SENSITIVE_REQUEST_BODY_KEYS}
|
||||
return {
|
||||
k: REDACTED_BY_LITELM_STRING if _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
|
||||
}
|
||||
|
||||
|
||||
# Quoted-key form: ``"input"`` / ``'messages'`` / ``"prompt"`` followed by
|
||||
|
|
|
|||
|
|
@ -612,7 +612,7 @@ def test_sanitize_request_body_for_spend_logs_payload_mixed_types():
|
|||
request_body = {
|
||||
"text": long_string,
|
||||
"number": 42,
|
||||
"nested": {"list": ["short", long_string], "dict": {"key": long_string}},
|
||||
"nested": {"list": ["short", long_string], "dict": {"value": long_string}},
|
||||
}
|
||||
sanitized = _sanitize_request_body_for_spend_logs_payload(request_body)
|
||||
|
||||
|
|
@ -631,7 +631,7 @@ def test_sanitize_request_body_for_spend_logs_payload_mixed_types():
|
|||
assert sanitized["number"] == 42
|
||||
assert sanitized["nested"]["list"][0] == "short"
|
||||
assert len(sanitized["nested"]["list"][1]) == expected_length
|
||||
assert len(sanitized["nested"]["dict"]["key"]) == expected_length
|
||||
assert len(sanitized["nested"]["dict"]["value"]) == expected_length
|
||||
|
||||
|
||||
def test_sanitize_request_body_for_spend_logs_payload_uses_runtime_env_override(
|
||||
|
|
@ -1207,7 +1207,7 @@ def test_get_logging_payload_placeholders_the_metadata_copied_into_the_stored_re
|
|||
stored_request_body: Final = json.loads(payload["proxy_server_request"])
|
||||
assert stored_request_body["metadata"]["model_group"] == expected_stored_model_group
|
||||
assert stored_request_body["metadata"]["error_information"]["error_message"] == expected_stored_error_message
|
||||
assert stored_request_body["metadata"]["user_api_key"] == "sk-test"
|
||||
assert stored_request_body["metadata"]["user_api_key"] == REDACTED_BY_LITELM_STRING
|
||||
assert ("medical records" in payload["proxy_server_request"]) == bool(deployment_info)
|
||||
|
||||
|
||||
|
|
@ -2691,6 +2691,104 @@ def test_sanitize_request_body_strips_secret_fields():
|
|||
assert sanitized["messages"] == [{"role": "user", "content": "hi"}]
|
||||
|
||||
|
||||
@patch("litellm.proxy.spend_tracking.spend_tracking_utils.should_store_prompts_and_responses_in_spend_logs")
|
||||
def test_proxy_server_request_payload_strips_nested_aws_credentials(mock_should_store: MagicMock) -> None:
|
||||
mock_should_store.return_value = True
|
||||
credentials: Final = {
|
||||
"aws_access_key_id": "AKIA-canary",
|
||||
"aws_secret_access_key": "secret-canary",
|
||||
"aws_session_token": "token-canary",
|
||||
"aws_web_identity_token": "wit-canary",
|
||||
}
|
||||
tool_parameters: Final = {"type": "object", "properties": {"aws_secret_access_key": {"type": "string"}}}
|
||||
litellm_params: Final = {
|
||||
"proxy_server_request": {
|
||||
"body": {
|
||||
"model": "bedrock-claude",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"fallbacks": [{"model": "bedrock-b", "aws_region_name": "us-west-2", **credentials}],
|
||||
"extra_body": {"aws_role_name": "arn:aws:iam::123456789012:role/r", **credentials},
|
||||
"tools": [{"type": "function", "function": {"name": "f", "parameters": tool_parameters}}],
|
||||
**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)
|
||||
masked: Final = dict.fromkeys(credentials, REDACTED_BY_LITELM_STRING)
|
||||
assert parsed["fallbacks"] == [{"model": "bedrock-b", "aws_region_name": "us-west-2", **masked}]
|
||||
assert parsed["extra_body"] == {"aws_role_name": "arn:aws:iam::123456789012:role/r", **masked}
|
||||
assert {name: parsed[name] for name in credentials} == masked
|
||||
assert parsed["tools"][0]["function"]["parameters"] == tool_parameters
|
||||
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",
|
||||
}
|
||||
metadata: Final = {"user_api_key": "custom-auth-raw-key", "requester_ip_address": "10.0.0.1"}
|
||||
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,
|
||||
"prompt_cache_key": "user-123-cache",
|
||||
"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": 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"] == {"user_api_key": REDACTED_BY_LITELM_STRING, "requester_ip_address": "10.0.0.1"}
|
||||
assert parsed["max_tokens"] == 10
|
||||
assert parsed["prompt_cache_key"] == REDACTED_BY_LITELM_STRING
|
||||
assert parsed["messages"] == [{"role": "user", "content": "hello"}]
|
||||
|
||||
|
||||
def test_sanitize_response_redacts_credential_named_fields() -> None:
|
||||
response: Final = {"access_token": "canary-oauth-token", "usage": {"prompt_tokens": 1}}
|
||||
|
||||
assert _sanitize_request_body_for_spend_logs_payload({"response": response}) == {
|
||||
"response": {"access_token": REDACTED_BY_LITELM_STRING, "usage": {"prompt_tokens": 1}}
|
||||
}
|
||||
|
||||
|
||||
@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