fix(cli): tighten mouse leak sanitizer

Handle unbounded SGR mouse report coordinates and avoid regex work on ordinary prompt-buffer edits by short-circuiting before sanitizer passes.
This commit is contained in:
Brooklyn Nicholson
2026-04-29 22:10:18 -05:00
parent d05497f812
commit 87e259a678
2 changed files with 27 additions and 13 deletions

36
cli.py
View File

@@ -1540,13 +1540,14 @@ def _strip_leaked_bracketed_paste_wrappers(text: str) -> str:
# that appears when the ESC byte was stripped by a prior filter. # that appears when the ESC byte was stripped by a prior filter.
_DSR_CPR_ESC_RE = re.compile(r"\x1b\[\d+;\d+R") _DSR_CPR_ESC_RE = re.compile(r"\x1b\[\d+;\d+R")
_DSR_CPR_VISIBLE_RE = re.compile(r"\^\[\[\d+;\d+R") _DSR_CPR_VISIBLE_RE = re.compile(r"\^\[\[\d+;\d+R")
_SGR_MOUSE_ESC_RE = re.compile(r"\x1b\[<\d{1,3};\d{1,4};\d{1,4}[Mm]") _SGR_MOUSE_ESC_RE = re.compile(r"\x1b\[<\d+;\d+;\d+[Mm]")
_SGR_MOUSE_VISIBLE_RE = re.compile(r"\^\[\[<\d{1,3};\d{1,4};\d{1,4}[Mm]") _SGR_MOUSE_VISIBLE_RE = re.compile(r"\^\[\[<\d+;\d+;\d+[Mm]")
# Some terminals/filters can drop ESC and literal "^[[", leaving only # Some terminals/filters can drop ESC and literal "^[[", leaving only
# "<btn;col;rowM" fragments in the buffer. Keep this broad on purpose: # "<btn;col;rowM" fragments in the buffer. Keep this broad on purpose:
# these fragments are extremely unlikely to be intentional user input, and # these fragments are extremely unlikely to be intentional user input, and
# stripping them is better than sending corrupted prompts. # stripping them is better than sending corrupted prompts.
_SGR_MOUSE_BARE_RE = re.compile(r"<\d{1,3};\d{1,4};\d{1,4}[Mm]") _SGR_MOUSE_BARE_RE = re.compile(r"<\d+;\d+;\d+[Mm]")
_TERMINAL_RESPONSE_SENTINELS = ("\x1b[", "^[", "<")
_TERMINAL_INPUT_MODE_RESET_SEQ = ( _TERMINAL_INPUT_MODE_RESET_SEQ = (
"\x1b[?1006l" # disable SGR mouse "\x1b[?1006l" # disable SGR mouse
"\x1b[?1003l" # disable any-motion tracking "\x1b[?1003l" # disable any-motion tracking
@@ -1577,16 +1578,25 @@ def _strip_leaked_terminal_responses_with_meta(text: str) -> tuple[str, bool]:
""" """
if not text: if not text:
return text, False return text, False
had_mouse_reports = bool( if not any(marker in text for marker in _TERMINAL_RESPONSE_SENTINELS):
_SGR_MOUSE_ESC_RE.search(text) return text, False
or _SGR_MOUSE_VISIBLE_RE.search(text)
or _SGR_MOUSE_BARE_RE.search(text) had_mouse_reports = False
)
text = _DSR_CPR_ESC_RE.sub("", text) if "\x1b[" in text:
text = _DSR_CPR_VISIBLE_RE.sub("", text) text = _DSR_CPR_ESC_RE.sub("", text)
text = _SGR_MOUSE_ESC_RE.sub("", text) text, count = _SGR_MOUSE_ESC_RE.subn("", text)
text = _SGR_MOUSE_VISIBLE_RE.sub("", text) had_mouse_reports = had_mouse_reports or count > 0
text = _SGR_MOUSE_BARE_RE.sub("", text)
if "^[" in text:
text = _DSR_CPR_VISIBLE_RE.sub("", text)
text, count = _SGR_MOUSE_VISIBLE_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0
if "<" in text:
text, count = _SGR_MOUSE_BARE_RE.subn("", text)
had_mouse_reports = had_mouse_reports or count > 0
return text, had_mouse_reports return text, had_mouse_reports

View File

@@ -68,6 +68,10 @@ class TestStripLeakedTerminalResponses:
text = "abc<65;1;49Mdef" text = "abc<65;1;49Mdef"
assert _strip_leaked_terminal_responses(text) == "abcdef" assert _strip_leaked_terminal_responses(text) == "abcdef"
def test_strips_sgr_mouse_report_with_large_coordinates(self):
text = "abc\x1b[<10000;12345;98765Mdef"
assert _strip_leaked_terminal_responses(text) == "abcdef"
def test_strips_multiple_concatenated_sgr_mouse_reports(self): def test_strips_multiple_concatenated_sgr_mouse_reports(self):
text = "<65;1;49M<35;1;42Mhello<64;1;40m" text = "<65;1;49M<35;1;42Mhello<64;1;40m"
assert _strip_leaked_terminal_responses(text) == "hello" assert _strip_leaked_terminal_responses(text) == "hello"