Skip to content

Commit 7ec9dcd

Browse files
committed
fix(tui): strip ANSI/control sequences from diff card bodies
render_diff rendered untrusted file content and model-supplied edit text verbatim, so a crafted edit could smuggle ANSI escapes (cursor movement, color) into the terminal through Update/Write diff cards and the approval/ pager diffs that share the renderer. Sanitize diff_text once at the top of render_diff; sanitize_ansi keeps newlines/tabs so +/- prefix and line-number parsing are unaffected, and visible text is preserved.
1 parent 06f067b commit 7ec9dcd

3 files changed

Lines changed: 23 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ GitHub Releases page; `0.8.0` is the new starting line.
1515

1616
## Unreleased
1717

18+
- **TUI: diff cards strip terminal control sequences.** Inline file-diff bodies
19+
(Update/Write cards, approval and pager diffs) now sanitize ANSI/control escapes
20+
from the untrusted file and model-supplied edit content before rendering, so a
21+
crafted edit can no longer smuggle cursor-movement or color escapes into the
22+
terminal through a diff card. Visible text is preserved.
1823
- **TUI: interactive resize/handoff ghosting.** Scrollback handoffs in prompt mode
1924
now fully suppress the transient preamble (agent stream body, verb spinner, and
2025
tips) while ``run_in_terminal`` emits permanent scrollback, so stacked

src/pythinker_code/ui/shell/components/diff.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from rich.table import Table
2626
from rich.text import Text
2727

28+
from pythinker_code.ui.shell.components.render_utils import sanitize_ansi
2829
from pythinker_code.ui.shell.render_constants import (
2930
DIFF_CONTEXT_LINES,
3031
DIFF_LINE_NUMBER_MIN_WIDTH,
@@ -403,6 +404,13 @@ def render_diff(diff_text: str, *, path: str | None = None) -> RenderableType:
403404
if not diff_text:
404405
return Text("")
405406

407+
# Diff bodies carry untrusted file content and model-supplied edit text. Strip
408+
# ANSI/control sequences before rendering so a crafted edit can't smuggle
409+
# cursor-movement or color escapes into the terminal through the diff card.
410+
# sanitize_ansi keeps newlines and tabs, so +/- prefix and line-number parsing
411+
# below is unaffected.
412+
diff_text = sanitize_ansi(diff_text)
413+
406414
colors = get_diff_colors()
407415
# Added/removed rows are distinguished by background tint only; line numbers,
408416
# +/- markers, and code content all use the terminal's default foreground

tests/ui_and_conv/test_tui_card_tool_renderers.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1082,6 +1082,16 @@ def test_render_diff_colorizes_added_removed():
10821082
assert "world" in plain
10831083

10841084

1085+
def test_render_diff_strips_ansi_control_sequences():
1086+
"""Diff bodies carry untrusted file/model content; a crafted edit must not
1087+
smuggle ANSI/control escapes into the terminal through the diff card."""
1088+
diff = "- safe\n+ \x1b[31mRED\x1b[0m\x07evil"
1089+
plain = render_plain(render_diff(diff, path="/x.py"), width=80)
1090+
assert "\x1b" not in plain # CSI escape stripped
1091+
assert "\x07" not in plain # BEL stripped
1092+
assert "RED" in plain and "evil" in plain # visible text preserved
1093+
1094+
10851095
def test_render_diff_spaces_marker_before_at_rule():
10861096
old = "@keyframes drawer-fade-in { from { opacity: 0; } to { opacity: 1); } }\n"
10871097
new = "@keyframes drawer-fade-in { from { opacity: 0; } to { opacity: 1; } }\n"

0 commit comments

Comments
 (0)