mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
fix(logging): keep SGR colour codes when sanitizing verbose stdout
This commit is contained in:
parent
ca6ddfe93e
commit
3feb2bf27c
2 changed files with 20 additions and 10 deletions
|
|
@ -44,16 +44,18 @@ def redact_secrets(value: str) -> str:
|
|||
return _redact_string(value)
|
||||
|
||||
|
||||
_CONTROL_CHAR_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f-\x9f\u2028\u2029]")
|
||||
_SGR_COLOR_OR_CONTROL_CHAR_RE = re.compile(r"(\x1b\[[0-9;]{0,16}m)|([\x00-\x08\x0a-\x1f\x7f-\x9f\u2028\u2029])")
|
||||
_CONTROL_CHAR_NAMES = {"\n": "\\n", "\r": "\\r", "\x0b": "\\v", "\x0c": "\\f", "\x08": "\\b", "\x07": "\\a"}
|
||||
|
||||
|
||||
def _escape_control_character(match: "re.Match[str]") -> str:
|
||||
char = match.group()
|
||||
named = _CONTROL_CHAR_NAMES.get(char)
|
||||
sgr_color_sequence, control_char = match.groups()
|
||||
if sgr_color_sequence is not None:
|
||||
return sgr_color_sequence
|
||||
named = _CONTROL_CHAR_NAMES.get(control_char)
|
||||
if named is not None:
|
||||
return named
|
||||
codepoint = ord(char)
|
||||
codepoint = ord(control_char)
|
||||
return f"\\x{codepoint:02x}" if codepoint < 0x100 else f"\\u{codepoint:04x}"
|
||||
|
||||
|
||||
|
|
@ -61,9 +63,10 @@ def sanitize_control_characters(value: str) -> str:
|
|||
"""Escape characters that let untrusted content forge log lines or drive the terminal.
|
||||
|
||||
Covers C0/C1 controls (CR, LF, ESC, ...) and the Unicode line/paragraph
|
||||
separators; tab is left alone since it cannot start a new log record.
|
||||
separators. Tab survives since it cannot start a new log record, and so do
|
||||
SGR colour sequences, which litellm itself emits to colourise verbose output.
|
||||
"""
|
||||
return _CONTROL_CHAR_RE.sub(_escape_control_character, value)
|
||||
return _SGR_COLOR_OR_CONTROL_CHAR_RE.sub(_escape_control_character, value)
|
||||
|
||||
|
||||
def redact_and_sanitize(print_statement: object) -> str:
|
||||
|
|
|
|||
|
|
@ -392,20 +392,27 @@ def test_normal_vertex_log_not_redacted():
|
|||
|
||||
|
||||
def test_sanitize_control_characters_escapes_injection_vectors():
|
||||
raw = "line1\r\n2026-01-01 INFO forged\x1b[31mred\x1b[0m\u2028\u2029\x00 tab\there"
|
||||
raw = "line1\r\n2026-01-01 INFO forged\x1b]0;window title\x07\x1b[2J\u2028\u2029\x00 tab\there"
|
||||
result = sanitize_control_characters(raw)
|
||||
assert "\n" not in result
|
||||
assert "\r" not in result
|
||||
assert "\x1b" not in result
|
||||
assert "\x07" not in result
|
||||
assert "\u2028" not in result
|
||||
assert "\u2029" not in result
|
||||
assert "\x00" not in result
|
||||
assert "\\r\\n" in result
|
||||
assert "\\x1b[31mred" in result
|
||||
assert "\\x1b]0;window title\\a\\x1b[2J" in result
|
||||
assert "\\u2028\\u2029\\x00" in result
|
||||
assert "tab\there" in result
|
||||
|
||||
|
||||
def test_sanitize_control_characters_keeps_litellm_colour_codes():
|
||||
"""litellm colourises its own verbose prints; SGR sequences stay intact."""
|
||||
coloured = "\x1b[92mRequest to litellm:\x1b[0m"
|
||||
assert sanitize_control_characters(coloured) == coloured
|
||||
|
||||
|
||||
def test_redact_and_sanitize_masks_secrets_and_control_chars():
|
||||
result = redact_and_sanitize({"api_key": SECRET, "model": "gpt-5", "note": "a\nb"})
|
||||
assert SECRET not in result
|
||||
|
|
@ -438,7 +445,7 @@ def test_print_verbose_stdout_is_redacted_and_sanitized(module_path, attribute_p
|
|||
module = importlib.import_module(module_path)
|
||||
attribute_names = attribute_path.split(".")
|
||||
print_verbose = functools.reduce(getattr, attribute_names, module)
|
||||
payload = f"api_key={SECRET} chunk=gpt-5\r\n2026-01-01 INFO forged line \x1b[31mred\x1b[0m"
|
||||
payload = f"api_key={SECRET} chunk=gpt-5\r\n2026-01-01 INFO forged line \x1b]0;pwned\x07"
|
||||
arguments = (None, payload) if len(attribute_names) > 1 else (payload,)
|
||||
|
||||
with patch.object(litellm, "set_verbose", True):
|
||||
|
|
@ -452,4 +459,4 @@ def test_print_verbose_stdout_is_redacted_and_sanitized(module_path, attribute_p
|
|||
assert "\x1b" not in out
|
||||
assert out.count("\n") == 1
|
||||
assert "\\r\\n" in out
|
||||
assert "\\x1b[31mred" in out
|
||||
assert "\\x1b]0;pwned\\a" in out
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue