Skip to content

fix(output,tui): neutralize control sequences and markup in server text (#206) - #207

Merged
thomaschristory merged 2 commits into
mainfrom
fix/sanitize-terminal-output
Oct 24, 2026
Merged

thomaschristory merged 2 commits into
mainfrom
fix/sanitize-terminal-output

Conversation

@thomaschristory

Copy link
Copy Markdown
Owner

Fixes #206.

Rich's escape() only stops [..] markup, and Rich and Textual strip just BEL, BS, VT, FF and CR. ESC sequences, other C0/C1 controls and bidi overrides in NetBox data therefore reached the terminal: an object name could retitle or clear it, or spoof text.

Shared helper: nsc/output/sanitize.py, moved out of errors.py from #205:

  • printable() shows control and format characters as \x../\u..... Newlines, tabs and ordinary Unicode are kept.
  • safe_markup() = escape(printable(...)), for markup strings.
  • safe_text() builds a Text, for widgets that would parse a str as markup.

Applied to:

  • Table output: cells, and column headers, which weren't escaped at all.
  • --explain panel and the error panel.
  • TUI:
    • list and detail cells; detail keys are server JSON keys;
    • record-picker labels;
    • api_error_message, used by every notify, which parses markup;
    • the diff, bulk-diff and bulk-summary views.

TUI markup injection, found along the way: Textual's DataTable and Label parse a plain str as markup. A NetBox value [bold red]x[/] showed as a bold red "x" with its brackets gone. TUI cells are now Text, so they render literally. I confirmed this in a headless Textual run before the change. Four existing tests asserted the old "cells are plain str" contract; I updated them to compare .plain.

Out of scope: JSON, JSONL, YAML and CSV output (escaped or quoted, or not for a terminal). TUI titles, form labels and filter chips, which hold schema names or the user's own input.

Tests:

  • tests/output/test_sanitize.py: the helper; table cells and headers with color on and off; headers not parsed as markup; the explain panel.
  • tests/tui/test_sanitize_tui.py: render_cell, api_error_message, detail-screen cells, record-picker labels.

The new tests failed before the fix.

https://claude.ai/code/session_01RD8NgCVtp3yYFd6NSZpbUb

…xt (#206)

Rich's escape() only stops [..] markup, and Rich/Textual strip just BEL,
BS, VT, FF and CR, so ESC sequences and bidi overrides in NetBox data
reached the terminal (retitle/clear it, spoof text).

Move the sanitizer to nsc/output/sanitize.py (printable, safe_markup,
safe_text) and apply it to table cells and headers (headers were not
escaped at all), the --explain panel, error panels, and the TUI: list
and detail cells, record picker labels, error notifications, and diff
views. TUI cells are now Text, not str: DataTable/Label parsed str as
markup, so a value like `[red]x[/]` was styled and lost its brackets.

Claude-Session: https://claude.ai/code/session_01RD8NgCVtp3yYFd6NSZpbUb
Review of #207 found more widgets that parse a str label as markup and
take server text: global-search tree leaves, FK buttons in the edit,
bulk-edit and filter forms (initial label and after picking), and the
column chooser (whose literal "[x]" mark was itself parsed as markup).

Claude-Session: https://claude.ai/code/session_01RD8NgCVtp3yYFd6NSZpbUb
@thomaschristory

Copy link
Copy Markdown
Owner Author

Pushed 6d58c2f: review found more markup-parsing sinks with server text (global search leaves, FK buttons in edit/bulk-edit/filter forms, column chooser). All now safe_text; tests added.

@thomaschristory
thomaschristory merged commit 45e017b into main Oct 24, 2026
13 checks passed
@thomaschristory
thomaschristory deleted the fix/sanitize-terminal-output branch October 24, 2026 22:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Table output doesn't neutralize terminal control characters in server data

1 participant