mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-07 02:59:05 +00:00
resolved greptile issues
This commit is contained in:
parent
82204d3e97
commit
a8da002953
4 changed files with 135 additions and 31 deletions
|
|
@ -34,6 +34,7 @@ from litellm.types.utils import (
|
|||
StandardLoggingUserAPIKeyMetadata,
|
||||
SupportedCacheControls,
|
||||
)
|
||||
from litellm.utils import _mask_secret_fields_for_logging
|
||||
|
||||
service_logger_obj = ServiceLogging() # used for tracking latency on OTEL
|
||||
|
||||
|
|
@ -252,34 +253,6 @@ def clean_headers(
|
|||
return clean_headers
|
||||
|
||||
|
||||
def _mask_secret_fields_for_logging(secret_fields: dict) -> dict:
|
||||
"""
|
||||
Masks sensitive values (like JWT tokens) in secret_fields before logging.
|
||||
Keeps the field structure intact but redacts authorization header values.
|
||||
"""
|
||||
_sensitive_header_keys = frozenset(
|
||||
{"authorization", "x-api-key", "api-key", "x-litellm-api-key"}
|
||||
)
|
||||
if not isinstance(secret_fields, dict):
|
||||
return secret_fields
|
||||
masked = {}
|
||||
for field_key, field_value in secret_fields.items():
|
||||
if isinstance(field_value, dict):
|
||||
masked_inner = {}
|
||||
for k, v in field_value.items():
|
||||
if k.lower() in _sensitive_header_keys and isinstance(v, str):
|
||||
if len(v) > 20:
|
||||
masked_inner[k] = v[:10] + "****" + v[-4:]
|
||||
else:
|
||||
masked_inner[k] = "****"
|
||||
else:
|
||||
masked_inner[k] = v
|
||||
masked[field_key] = masked_inner
|
||||
else:
|
||||
masked[field_key] = field_value
|
||||
return masked
|
||||
|
||||
|
||||
class LiteLLMProxyRequestSetup:
|
||||
@staticmethod
|
||||
def _get_timeout_from_request(headers: dict) -> Optional[float]:
|
||||
|
|
@ -927,7 +900,17 @@ async def add_litellm_data_to_request( # noqa: PLR0915
|
|||
|
||||
## Cache Controls
|
||||
headers = request.headers
|
||||
verbose_proxy_logger.debug("Request Headers: %s", clean_headers(headers))
|
||||
verbose_proxy_logger.debug(
|
||||
"Request Headers: %s",
|
||||
clean_headers(
|
||||
headers,
|
||||
litellm_key_header_name=(
|
||||
general_settings.get("litellm_key_header_name")
|
||||
if general_settings is not None
|
||||
else None
|
||||
),
|
||||
),
|
||||
)
|
||||
cache_control_header = headers.get("Cache-Control", None)
|
||||
if cache_control_header:
|
||||
cache_dict = parse_cache_control(cache_control_header)
|
||||
|
|
|
|||
|
|
@ -329,10 +329,10 @@ from litellm.proxy.hooks.prompt_injection_detection import (
|
|||
from litellm.proxy.hooks.proxy_track_cost_callback import _ProxyDBLogger
|
||||
from litellm.proxy.image_endpoints.endpoints import router as image_router
|
||||
from litellm.proxy.litellm_pre_call_utils import (
|
||||
_mask_secret_fields_for_logging,
|
||||
add_litellm_data_to_request,
|
||||
clean_headers,
|
||||
)
|
||||
from litellm.utils import _mask_secret_fields_for_logging
|
||||
from litellm.proxy.management_endpoints.budget_management_endpoints import (
|
||||
router as budget_management_router,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -7267,6 +7267,11 @@ def get_valid_models(
|
|||
return [] # NON-Blocking
|
||||
|
||||
|
||||
_SENSITIVE_HEADER_KEYS = frozenset(
|
||||
{"authorization", "x-api-key", "api-key", "x-litellm-api-key"}
|
||||
)
|
||||
|
||||
|
||||
def _mask_secret_fields_for_logging(secret_fields: dict) -> dict:
|
||||
"""
|
||||
Masks sensitive values (like JWT tokens) in secret_fields before logging.
|
||||
|
|
@ -7279,7 +7284,7 @@ def _mask_secret_fields_for_logging(secret_fields: dict) -> dict:
|
|||
if isinstance(field_value, dict):
|
||||
masked_inner = {}
|
||||
for k, v in field_value.items():
|
||||
if k.lower() in ("authorization", "x-api-key", "api-key", "x-litellm-api-key") and isinstance(v, str):
|
||||
if k.lower() in _SENSITIVE_HEADER_KEYS and isinstance(v, str):
|
||||
# Show first 10 and last 4 chars, mask the rest
|
||||
if len(v) > 20:
|
||||
masked_inner[k] = v[:10] + "****" + v[-4:]
|
||||
|
|
|
|||
116
tests/litellm/proxy/test_mask_secret_fields.py
Normal file
116
tests/litellm/proxy/test_mask_secret_fields.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
Tests for _mask_secret_fields_for_logging to ensure JWT tokens and other
|
||||
sensitive auth headers are masked in debug logs.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from litellm.utils import _mask_secret_fields_for_logging
|
||||
|
||||
|
||||
SAMPLE_JWT = (
|
||||
"Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImxpdGVsbG0tbW9jay1qd3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ"
|
||||
".eyJzdWIiOiJkZWZhdWx0X3VzZXJfaWQiLCJyb2xlcyI6WyJBRE1JTiJdLCJpYXQiOjE3NzA5NDM0NDR9"
|
||||
".Yqr0dbk3XbD7nGEz5KzMyHjIfPH_18DNPjXe1tpKw9HMEvKGgIZSNP8MAsDwPnUvjeTH26kIp6xmy"
|
||||
)
|
||||
|
||||
SAMPLE_SECRET_FIELDS = {
|
||||
"raw_headers": {
|
||||
"host": "localhost:4000",
|
||||
"user-agent": "curl/8.7.1",
|
||||
"accept": "*/*",
|
||||
"authorization": SAMPLE_JWT,
|
||||
"content-type": "application/json",
|
||||
"content-length": "96",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TestMaskSecretFieldsForLogging:
|
||||
"""Tests for the proxy version of _mask_secret_fields_for_logging."""
|
||||
|
||||
def test_should_mask_authorization_header(self):
|
||||
result = _mask_secret_fields_for_logging(SAMPLE_SECRET_FIELDS)
|
||||
auth_value = result["raw_headers"]["authorization"]
|
||||
# Should not contain the full JWT
|
||||
assert auth_value != SAMPLE_JWT
|
||||
# Should contain masking characters
|
||||
assert "****" in auth_value
|
||||
# Should preserve first 10 chars
|
||||
assert auth_value.startswith(SAMPLE_JWT[:10])
|
||||
# Should preserve last 4 chars
|
||||
assert auth_value.endswith(SAMPLE_JWT[-4:])
|
||||
|
||||
def test_should_not_mask_non_sensitive_headers(self):
|
||||
result = _mask_secret_fields_for_logging(SAMPLE_SECRET_FIELDS)
|
||||
assert result["raw_headers"]["host"] == "localhost:4000"
|
||||
assert result["raw_headers"]["user-agent"] == "curl/8.7.1"
|
||||
assert result["raw_headers"]["accept"] == "*/*"
|
||||
assert result["raw_headers"]["content-type"] == "application/json"
|
||||
assert result["raw_headers"]["content-length"] == "96"
|
||||
|
||||
def test_should_mask_x_api_key_header(self):
|
||||
secret_fields = {
|
||||
"raw_headers": {
|
||||
"x-api-key": "sk-very-secret-api-key-value-1234567890",
|
||||
"host": "localhost:4000",
|
||||
}
|
||||
}
|
||||
result = _mask_secret_fields_for_logging(secret_fields)
|
||||
assert result["raw_headers"]["x-api-key"] != "sk-very-secret-api-key-value-1234567890"
|
||||
assert "****" in result["raw_headers"]["x-api-key"]
|
||||
assert result["raw_headers"]["host"] == "localhost:4000"
|
||||
|
||||
def test_should_mask_api_key_header(self):
|
||||
secret_fields = {
|
||||
"raw_headers": {
|
||||
"API-Key": "some-long-azure-api-key-value-here",
|
||||
"host": "localhost:4000",
|
||||
}
|
||||
}
|
||||
result = _mask_secret_fields_for_logging(secret_fields)
|
||||
assert "****" in result["raw_headers"]["API-Key"]
|
||||
|
||||
def test_should_mask_short_auth_values(self):
|
||||
secret_fields = {
|
||||
"raw_headers": {
|
||||
"authorization": "short-token",
|
||||
}
|
||||
}
|
||||
result = _mask_secret_fields_for_logging(secret_fields)
|
||||
assert result["raw_headers"]["authorization"] == "****"
|
||||
|
||||
def test_should_handle_non_dict_input(self):
|
||||
assert _mask_secret_fields_for_logging("not a dict") == "not a dict"
|
||||
assert _mask_secret_fields_for_logging(123) == 123
|
||||
assert _mask_secret_fields_for_logging(None) is None
|
||||
|
||||
def test_should_handle_empty_dict(self):
|
||||
assert _mask_secret_fields_for_logging({}) == {}
|
||||
|
||||
def test_should_handle_non_dict_field_values(self):
|
||||
secret_fields = {"some_key": "some_string_value"}
|
||||
result = _mask_secret_fields_for_logging(secret_fields)
|
||||
assert result["some_key"] == "some_string_value"
|
||||
|
||||
def test_should_not_mutate_original(self):
|
||||
original = {
|
||||
"raw_headers": {
|
||||
"authorization": SAMPLE_JWT,
|
||||
"host": "localhost:4000",
|
||||
}
|
||||
}
|
||||
_mask_secret_fields_for_logging(original)
|
||||
# Original should be unchanged
|
||||
assert original["raw_headers"]["authorization"] == SAMPLE_JWT
|
||||
|
||||
|
||||
class TestMaskSecretFieldsReexport:
|
||||
"""Verify the re-export from litellm_pre_call_utils points to the same function."""
|
||||
|
||||
def test_should_be_same_function_as_proxy_reexport(self):
|
||||
from litellm.proxy.litellm_pre_call_utils import (
|
||||
_mask_secret_fields_for_logging as proxy_version,
|
||||
)
|
||||
|
||||
assert proxy_version is _mask_secret_fields_for_logging
|
||||
Loading…
Add table
Reference in a new issue