From a8da0029534286c7ba740a22de39c8d622638427 Mon Sep 17 00:00:00 2001 From: shivam Date: Thu, 12 Feb 2026 17:56:36 -0800 Subject: [PATCH] resolved greptile issues --- litellm/proxy/litellm_pre_call_utils.py | 41 ++----- litellm/proxy/proxy_server.py | 2 +- litellm/utils.py | 7 +- .../litellm/proxy/test_mask_secret_fields.py | 116 ++++++++++++++++++ 4 files changed, 135 insertions(+), 31 deletions(-) create mode 100644 tests/litellm/proxy/test_mask_secret_fields.py diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index b434a87868a..f3fc65aa4a1 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -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) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index dbdf1785e7d..3459f3823d9 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -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, ) diff --git a/litellm/utils.py b/litellm/utils.py index f8b80b89420..635c80a8950 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -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:] diff --git a/tests/litellm/proxy/test_mask_secret_fields.py b/tests/litellm/proxy/test_mask_secret_fields.py new file mode 100644 index 00000000000..b74127c20b9 --- /dev/null +++ b/tests/litellm/proxy/test_mask_secret_fields.py @@ -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