fix(logging): keep SGR colour codes when sanitizing verbose stdout

This commit is contained in:
Devin AI 2026-07-26 12:18:43 +00:00
parent ca6ddfe93e
commit 3feb2bf27c
2 changed files with 20 additions and 10 deletions

View file

@ -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:

View file

@ -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