Skip to content

[Fix]: Sanitize dangerous ANSI/OSC escape sequences in write_to_pty#174

Open
matiaspalmac wants to merge 2 commits intoTrixtyAI:mainfrom
matiaspalmac:fix/pty-sanitize-ansi
Open

[Fix]: Sanitize dangerous ANSI/OSC escape sequences in write_to_pty#174
matiaspalmac wants to merge 2 commits intoTrixtyAI:mainfrom
matiaspalmac:fix/pty-sanitize-ansi

Conversation

@matiaspalmac
Copy link
Copy Markdown
Contributor

Description

write_to_pty forwarded xterm.js onData output straight to the shell, so any OSC/DCS/APC/PM/SOS sequence that arrived via a paste or programmatic input (an AI reply, a README snippet, a log file) reached the TTY unchanged. These sequences are meant for shell-to-UI signalling and have no legitimate reason to flow in the other direction.

Concrete risks this closes:

  • OSC 52 silently copies attacker-controlled data to the system clipboard.
  • OSC 0/1/2 retitles the window to impersonate another app.
  • OSC 7 points the host at a bogus working directory.
  • DCS / APC / PM / SOS carry arbitrary payloads that permissive terminals may interpret.

Related Issue

Fixes #61

Context / Problem

xterm.js's onData callback is the sole input path for user-typed keys and pasted content. Pasted text is not distinguished from typed text at that layer, so an OSC 52 payload embedded in e.g. an AI-assistant response pasted into the terminal went straight through to the shell. The terminal emulator on the read path then honored the clipboard-set request.

Change

  • New pure helper sanitize_pty_input(&str) -> String — strips:
    • ESC ] (OSC), ESC P (DCS), ESC X (SOS), ESC ^ (PM), ESC _ (APC)
    • their 8-bit C1 codepoint equivalents (U+0090, U+0098, U+009D, U+009E, U+009F)
    • through the next String Terminator (ST = ESC \ or U+009C; BEL / U+0007 also closes an OSC).
  • Walks by codepoint, not by byte — UTF-8 continuation bytes inside legitimate codepoints (😀 contains 0x98) would otherwise be mistaken for a C1 introducer.
  • CSI (ESC [) is preserved because xterm.js emits it for arrow keys, bracketed paste (CSI 200 ~ / CSI 201 ~), mouse events, and other normal terminal input.
  • Bare ESC is preserved because vim, readline vi-mode, etc. depend on it.
  • Unterminated sequences are dropped through end-of-input so an attacker cannot smuggle a payload by splitting it across two write_to_pty calls.
  • write_to_pty pipes input through the sanitizer before writing to the shell.

Trade-offs

  • A user who wants to type an OSC 52 by hand no longer can — acceptable because no realistic keyboard workflow produces OSC, and the safe alternative (xterm's own clipboard integration) is unaffected.
  • No buffering between calls. A split OSC payload is discarded instead of stitched back together. This is the safe default; stitching would mean holding state across every single keystroke, which is the exact footgun we want to avoid.

Verification

  • 10 new unit tests covering plain text, CSI passthrough, OSC 52 / OSC 0 / DCS / APC / PM / SOS stripping, unterminated OSC, 8-bit C1 bytes, back-to-back sequences, bare ESC, and UTF-8 multibyte preservation.
  • cargo test --lib → 37 passed.
  • cargo clippy --lib --no-deps -- -D warnings → clean.
  • OS: Windows 11
  • Version: v1.0.10

Checklist

  • I have tested this fix thoroughly.
  • I have followed the project's coding guidelines.
  • My changes generate no new warnings or errors.
  • I have verified the fix on:
    • OS: Windows
    • Version: v1.0.10

`write_to_pty` forwarded xterm.js `onData` output straight to the
shell, including any OSC/DCS/APC/PM/SOS sequences that happened to be
in a paste. These sequences exist for shell-to-UI signalling and have
no legitimate reason to flow in the other direction:

- OSC 52 can silently copy attacker-controlled data to the clipboard.
- OSC 0/1/2 can retitle the window to impersonate another app.
- OSC 7 can point the host at a bogus working directory.
- DCS / APC / PM / SOS carry arbitrary payloads that a tolerant
  terminal may interpret.

The fix adds a pure `sanitize_pty_input` helper that walks the input
by codepoint (not by byte — UTF-8 continuation bytes for e.g. `😀`
contain 0x98, which would otherwise collide with C1-SOS) and drops
ESC]/ESCP/ESCX/ESC^/ESC_ sequences plus their 7-bit C1 codepoint
equivalents through the next String Terminator (`ST` = ESC\ or 0x9C;
BEL also closes an OSC). CSI (`ESC[`) is preserved because xterm.js
relies on it for arrow keys, bracketed paste, mouse events, and
normal terminal input.

Unterminated sequences are dropped through end-of-input so a
payload split across two `write_to_pty` calls cannot sneak through.

Fixes TrixtyAI#61
Copilot AI review requested due to automatic review settings April 21, 2026 04:44
@github-actions github-actions bot added bug Something isn't working dependencies Pull requests that update a dependency file labels Apr 21, 2026
@github-actions
Copy link
Copy Markdown

Thanks for the contribution! I'll review it as soon as possible. If you have still changes, please mark this PR as draft and all reviews will be cancelled. Tests reviews will be re-run only when the PR is marked as ready for review.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR mitigates terminal-input injection by sanitizing xterm.js onData input before it is written to the backend PTY, preventing OSC/DCS/APC/PM/SOS payloads (e.g., OSC 52 clipboard writes) from reaching the shell.

Changes:

  • Added sanitize_pty_input(&str) -> String to strip OSC/DCS/APC/PM/SOS (including C1 control-code equivalents) until ST/BEL as appropriate.
  • Updated write_to_pty to write sanitized input to the PTY.
  • Added unit tests covering pass-through behavior (plain text/CSI/bare ESC) and stripping behavior (OSC/DCS/APC/PM/SOS, unterminated sequences, back-to-back sequences).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/desktop/src-tauri/src/pty.rs Outdated
Comment thread apps/desktop/src-tauri/src/pty.rs Outdated
Copilot flagged the phrase '7-bit C1 byte' as misleading — the listed
values (U+0090/U+0098/U+009D/U+009E/U+009F) are C1 control code
points, typically described as 8-bit (or, in a Rust `&str`, as
multi-byte UTF-8 code points).

Reworded the doc and the inline comment to say 'C1 control code
points' and to make the UTF-8 encoding explicit (e.g. `U+009D`
encodes as `0xC2 0x9D`). No behavior change; tests still pass 10/10.
@matiaspalmac
Copy link
Copy Markdown
Contributor Author

Addressed Copilot review: reworded the doc and inline comment on sanitize_pty_input to say C1 control code points (with explicit UTF-8 encoding, e.g. U+009D0xC2 0x9D) instead of the misleading 7-bit C1 byte. No behavior change; cargo test --lib pty:: still passes 10/10. (8320266)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working dependencies Pull requests that update a dependency file

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Fix]: Sanitize dangerous ANSI/OSC escape sequences in write_to_pty

2 participants