diff --git a/litellm/proxy/spend_tracking/spend_tracking_utils.py b/litellm/proxy/spend_tracking/spend_tracking_utils.py index 4592add1032..692200b856c 100644 --- a/litellm/proxy/spend_tracking/spend_tracking_utils.py +++ b/litellm/proxy/spend_tracking/spend_tracking_utils.py @@ -64,7 +64,11 @@ def _is_master_key(api_key: str | None, _master_key: str | None) -> bool: return secrets.compare_digest(api_key, _master_key) -_HASHED_JWT_RE = re.compile(r"^hashed-jwt-[a-fA-F0-9]{64}$") +_HASHED_JWT_RE = re.compile(r"hashed-jwt-[a-fA-F0-9]{64}") + + +def _is_prehashed_key_shape(value: str) -> bool: + return is_valid_sha256_hash(value) or _HASHED_JWT_RE.fullmatch(value) is not None def _redact_logged_api_key(value: str | None, *, already_hashed: bool = False) -> str | None: @@ -73,9 +77,7 @@ def _redact_logged_api_key(value: str | None, *, already_hashed: bool = False) - stripped: Final = re.sub(r"(?i)^bearer ", "", value) if not stripped: return None - if already_hashed and is_valid_sha256_hash(stripped): - return stripped - if _HASHED_JWT_RE.match(stripped): + if already_hashed and _is_prehashed_key_shape(stripped): return stripped return hash_token(stripped) @@ -136,7 +138,7 @@ def _get_spend_logs_metadata( _raw_key: Final = clean_metadata.get("user_api_key") _trusted_hash: Final = metadata.get("user_api_key_hash") _already_hashed: Final = ( - isinstance(_trusted_hash, str) and is_valid_sha256_hash(_trusted_hash) and _trusted_hash == _raw_key + isinstance(_trusted_hash, str) and _is_prehashed_key_shape(_trusted_hash) and _trusted_hash == _raw_key ) clean_metadata["user_api_key"] = _redact_logged_api_key(_raw_key, already_hashed=_already_hashed) clean_metadata["applied_guardrails"] = applied_guardrails @@ -296,7 +298,7 @@ def get_logging_payload(kwargs, response_obj, start_time, end_time) -> SpendLogs standard_logging_total_tokens = standard_logging_payload.get("total_tokens", 0) _trusted_hash = metadata.get("user_api_key_hash") _key_already_hashed = ( - isinstance(_trusted_hash, str) and is_valid_sha256_hash(_trusted_hash) and _trusted_hash == api_key + isinstance(_trusted_hash, str) and _is_prehashed_key_shape(_trusted_hash) and _trusted_hash == api_key ) api_key = _redact_logged_api_key(api_key, already_hashed=_key_already_hashed) or "" diff --git a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py index e34d4f389e1..92e78e04512 100644 --- a/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py +++ b/tests/test_litellm/proxy/spend_tracking/test_spend_tracking_utils.py @@ -2653,10 +2653,24 @@ def test_redact_logged_api_key_long_opaque_token_is_hashed(): def test_redact_logged_api_key_hashed_jwt_passes_through(): jwt_hash = "hashed-jwt-" + "a" * 64 - result = _redact_logged_api_key(jwt_hash) + result = _redact_logged_api_key(jwt_hash, already_hashed=True) assert result == jwt_hash +def test_redact_logged_api_key_hashed_jwt_shape_without_provenance_is_hashed(): + lookalike = "hashed-jwt-" + "a" * 64 + result = _redact_logged_api_key(lookalike) + assert result == hash_token(lookalike) + assert result != lookalike + + +def test_redact_logged_api_key_hashed_jwt_trailing_newline_is_hashed(): + trailing = "hashed-jwt-" + "a" * 64 + "\n" + result = _redact_logged_api_key(trailing, already_hashed=True) + assert result == hash_token(trailing) + assert result != trailing + + def test_redact_logged_api_key_hashed_jwt_short_suffix_is_hashed(): short_jwt = "hashed-jwt-tooshort" result = _redact_logged_api_key(short_jwt) @@ -2730,10 +2744,17 @@ def test_get_spend_logs_metadata_provenance_bypass_requires_hash_match(): def test_get_spend_logs_metadata_hashed_jwt_unchanged(): jwt_hash = "hashed-jwt-" + "b" * 64 - meta = _get_spend_logs_metadata({"user_api_key": jwt_hash}) + meta = _get_spend_logs_metadata({"user_api_key": jwt_hash, "user_api_key_hash": jwt_hash}) assert meta["user_api_key"] == jwt_hash +def test_get_spend_logs_metadata_hashed_jwt_shape_without_provenance_is_hashed(): + lookalike = "hashed-jwt-" + "b" * 64 + meta = _get_spend_logs_metadata({"user_api_key": lookalike}) + assert meta["user_api_key"] == hash_token(lookalike) + assert meta["user_api_key"] != lookalike + + def test_get_spend_logs_metadata_none_key_is_none(): meta = _get_spend_logs_metadata({"user_api_key": None}) assert meta["user_api_key"] is None