redact platext jwt

This commit is contained in:
shivam 2026-02-27 12:52:37 -08:00
parent 88ccffccc8
commit f37e948677
2 changed files with 147 additions and 1 deletions

View file

@ -1,6 +1,7 @@
import ast
import logging
import os
import re
import sys
from datetime import datetime
from logging import Formatter
@ -179,14 +180,47 @@ else:
handler.setFormatter(formatter)
_BEARER_TOKEN_PATTERN = re.compile(
r"(Bearer\s+)[^\s'\",})]{8,}",
re.IGNORECASE,
)
_REDACTED_TOKEN = r"\1[REDACTED]"
class SensitiveLogFilter(logging.Filter):
"""
Redacts Bearer tokens from all log output so JWTs and API keys in
Authorization headers are never written to logs in plaintext.
Works by pre-formatting the log message (msg % args) and applying a regex
substitution before the record reaches the handler/formatter.
"""
def filter(self, record: logging.LogRecord) -> bool:
try:
formatted = record.getMessage()
if _BEARER_TOKEN_PATTERN.search(formatted):
record.msg = _BEARER_TOKEN_PATTERN.sub(_REDACTED_TOKEN, formatted)
record.args = None
except Exception:
pass
return True
_sensitive_filter = SensitiveLogFilter()
verbose_proxy_logger = logging.getLogger("LiteLLM Proxy")
verbose_router_logger = logging.getLogger("LiteLLM Router")
verbose_logger = logging.getLogger("LiteLLM")
# Add the handler to the logger
# Add the handler and the sensitive-data filter to every logger
verbose_router_logger.addHandler(handler)
verbose_router_logger.addFilter(_sensitive_filter)
verbose_proxy_logger.addHandler(handler)
verbose_proxy_logger.addFilter(_sensitive_filter)
verbose_logger.addHandler(handler)
verbose_logger.addFilter(_sensitive_filter)
def _suppress_loggers():

View file

@ -16,6 +16,7 @@ import litellm
from litellm._logging import (
ALL_LOGGERS,
JsonFormatter,
SensitiveLogFilter,
_initialize_loggers_with_handler,
_turn_on_json,
verbose_logger,
@ -262,3 +263,114 @@ async def test_cache_hit_includes_custom_llm_provider():
# Clean up
litellm.callbacks = original_callbacks
litellm.cache = None
# ---------------------------------------------------------------------------
# SensitiveLogFilter tests
# ---------------------------------------------------------------------------
def _make_record(msg, args=(), level=logging.DEBUG, name="LiteLLM"):
"""Helper to create a LogRecord for filter tests."""
return logging.LogRecord(
name=name,
level=level,
pathname="",
lineno=0,
msg=msg,
args=args,
exc_info=None,
)
class TestSensitiveLogFilter:
"""Tests for the SensitiveLogFilter that redacts Bearer tokens."""
def setup_method(self):
self.filt = SensitiveLogFilter()
def test_should_redact_bearer_token_in_plain_message(self):
record = _make_record(
"Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.sig"
)
self.filt.filter(record)
msg = record.getMessage()
assert "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9" not in msg
assert "Bearer [REDACTED]" in msg
def test_should_redact_bearer_in_dict_format_arg(self):
data = {
"model": "gpt-4",
"secret_fields": {
"raw_headers": {
"authorization": "Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIn0.abc"
}
},
}
record = _make_record("receiving data: %s", (data,))
self.filt.filter(record)
msg = record.getMessage()
assert "eyJhbGciOiJSUzI1NiJ9" not in msg
assert "Bearer [REDACTED]" in msg
assert "gpt-4" in msg
def test_should_leave_non_sensitive_messages_unchanged(self):
record = _make_record("Cache hit for model gpt-4, returning cached response")
self.filt.filter(record)
assert record.getMessage() == "Cache hit for model gpt-4, returning cached response"
def test_should_handle_nested_dict_with_authorization(self):
data = {
"proxy_server_request": {
"url": "http://localhost:4000/v1/chat/completions",
"method": "POST",
"headers": {
"authorization": "Bearer sk-proj-abc123def456"
},
}
}
record = _make_record("receiving data: %s", (data,))
self.filt.filter(record)
msg = record.getMessage()
assert "sk-proj-abc123def456" not in msg
assert "Bearer [REDACTED]" in msg
def test_should_not_redact_bearer_word_alone(self):
record = _make_record("The bearer of this message is authorized")
self.filt.filter(record)
assert record.getMessage() == "The bearer of this message is authorized"
def test_should_redact_multiple_tokens_in_one_message(self):
record = _make_record(
"Header1: Bearer tokenAAA, Header2: Bearer tokenBBB"
)
self.filt.filter(record)
msg = record.getMessage()
assert "tokenAAA" not in msg
assert "tokenBBB" not in msg
assert msg.count("Bearer [REDACTED]") == 2
def test_should_be_case_insensitive(self):
record = _make_record("auth: bearer eyJsecretPayload.data.sig")
self.filt.filter(record)
msg = record.getMessage()
assert "eyJsecretPayload" not in msg
assert "[REDACTED]" in msg
def test_should_always_return_true(self):
record = _make_record("Bearer supersecrettoken123")
result = self.filt.filter(record)
assert result is True
def test_should_preserve_args_when_no_token_present(self):
record = _make_record("model=%s tokens=%d", ("gpt-4", 150))
self.filt.filter(record)
assert record.args == ("gpt-4", 150)
assert record.getMessage() == "model=gpt-4 tokens=150"
def test_should_clear_args_after_redaction(self):
record = _make_record("auth: %s", ("Bearer my_jwt_token_here",))
self.filt.filter(record)
assert record.args is None
msg = record.getMessage()
assert "my_jwt_token_here" not in msg
assert "Bearer [REDACTED]" in msg