fix: Filter terminal escape sequence responses in PTY sessions#77
fix: Filter terminal escape sequence responses in PTY sessions#77
Conversation
When running terminal applications like Neovim via SSH, raw terminal capability query responses (XTGETTCAP, DA1/DA2/DA3, OSC) were appearing as visible text on screen instead of being properly handled. Changes: - Add escape_filter module with state machine to filter terminal responses - Set TERM and COLORTERM environment variables before PTY allocation - Filter XTGETTCAP, DA responses, OSC responses, and DCS sequences - Preserve valid escape sequences for colors, cursor movement, etc. - Add comprehensive test suite for escape sequence filtering This ensures interactive applications work cleanly without visual artifacts. Fixes #76
Security & Performance Review: PR #77Analysis Summary
Prioritized Issue RoadmapHIGH - State Machine Can Buffer Indefinitely in CSI StateFile: Issue: In the Example Attack Vector: Impact:
Recommendation: Add a reasonable limit check earlier in CSI/CsiQuestion states (e.g., CSI parameters rarely exceed 100 bytes): // In Csi and CsiQuestion states, add:
if self.pending_buffer.len() > 256 {
// Malformed CSI - flush and reset
output.extend_from_slice(&self.pending_buffer);
self.pending_buffer.clear();
self.state = FilterState::Normal;
}HIGH - PtyMessage::RemoteOutput Path Not FilteredFile: Issue: The Current Code: Some(PtyMessage::RemoteOutput(data)) => {
// Write directly to stdout for better performance
if let Err(e) = io::stdout().write_all(&data) {Recommendation: Either:
MEDIUM - DCS Sequences Over-filteredFile: Issue: All DCS sequences (ESC P ...) are filtered by default, including those that are not responses. While XTGETTCAP responses should be filtered, some DCS sequences are legitimate terminal commands (like DECRQSS responses, sixel graphics setup, etc.). Impact: Potential loss of legitimate terminal functionality that uses DCS sequences. Recommendation: Be more selective about which DCS sequences to filter: FilterState::Dcs => {
self.pending_buffer.push(byte);
if byte == b'+' {
self.state = FilterState::DcsPlus;
} else if byte == b'$' {
// DECRQSS response - filter
self.state = FilterState::DcsResponse;
} else if byte == 0x1b {
self.state = FilterState::St;
} else if byte == 0x07 {
// Only filter known response patterns
if self.is_dcs_response() {
// filter
} else {
output.extend_from_slice(&self.pending_buffer);
}
self.pending_buffer.clear();
self.state = FilterState::Normal;
}
}MEDIUM - State Persistence After Filter CallFile: Issue: The filter maintains state ( Scenario:
Actually, looking more carefully, this is handled correctly. The issue is more subtle - if we receive Recommendation: Consider adding a timestamp-based flush mechanism for very old pending sequences, or document this limitation. MEDIUM - OSC Parameter Parsing Integer Overflow RiskFile: Issue: The Current Code: let param_str = String::from_utf8_lossy(&self.pending_buffer[start..end]);
if let Ok(param) = param_str.parse::<u32>() {
matches!(param, 4 | 10..=19 | 52)
}Impact: Minor - the 4KB buffer limit prevents truly pathological cases, but the allocation of String via Recommendation: Parse directly from bytes to avoid allocation: fn parse_osc_param(&self) -> Option<u32> {
let start = 2;
let mut end = start;
let mut value: u32 = 0;
while end < self.pending_buffer.len() && end - start < 10 {
let b = self.pending_buffer[end];
if b.is_ascii_digit() {
value = value.checked_mul(10)?.checked_add((b - b'0') as u32)?;
end += 1;
} else {
break;
}
}
if end > start { Some(value) } else { None }
}LOW - Missing Test for Buffer Overflow ProtectionFile: Issue: The MAX_PENDING_SIZE overflow protection logic (line 299-308) is not tested. This is a critical safety mechanism that should have explicit test coverage. Recommendation: Add test: #[test]
fn test_buffer_overflow_protection() {
let mut filter = EscapeSequenceFilter::new();
// Start a DCS sequence that never terminates
let mut input = vec![0x1b, b'P'];
// Add enough data to exceed MAX_PENDING_SIZE (4096)
input.extend(vec![b'x'; 4100]);
let output = filter.filter(&input);
// After overflow, buffer should be flushed to output
assert!(!output.is_empty(), "Overflow protection should flush buffer");
}Additional Observations (Informational)
Manual Review Required
SummaryThe implementation is generally well-designed with good safety measures. The two HIGH priority issues should be addressed before merging:
The MEDIUM issues are worth addressing for robustness but are not blockers. |
- Add MAX_CSI_SEQUENCE_SIZE limit (256 bytes) to prevent malformed CSI sequences from buffering indefinitely in Csi and CsiQuestion states - Apply escape sequence filter to PtyMessage::RemoteOutput path for consistent filtering across all output channels - Add comprehensive tests for buffer overflow protection and malformed CSI sequence handling - Fix clippy warning by using derive(Default) for StrictHostKeyChecking These changes address HIGH priority issues from code review: 1. State machine could buffer indefinitely with malformed CSI input 2. RemoteOutput path was bypassing escape sequence filter
Additional Fixes from Code ReviewAdded the following improvements based on pr-reviewer feedback: HIGH Priority Fixes
Additional Improvements
All 14 escape filter tests pass (up from 11). |
- Only filter known DCS response sequences (XTGETTCAP: +r, DECRQSS: $) - Pass through non-response DCS sequences (sixel graphics, DECUDK, etc.) - Add DcsDecrqss and DcsPassthrough states for proper state tracking - Add is_dcs_response() helper method - Add 4 new tests for DCS passthrough and filtering behavior
- Add sequence_start timestamp to track when sequences begin - Flush incomplete sequences after 500ms timeout on next filter call - Add reset_to_normal() helper for consistent state cleanup - Add helper methods: has_timed_out_sequence(), flush_pending() - Add 4 new tests for timeout and timestamp functionality
- Replace String::from_utf8_lossy with direct byte-to-u32 parsing - Add parse_osc_param() helper method for efficient parameter extraction - Use checked_mul/checked_add for overflow protection (max 10 digits) - Add 3 new tests for OSC parameter parsing
- Remove unconditional filtering of Device Attributes (DA) responses - DA responses (\x1b[?...c) are needed by ncurses/htop for terminal capability detection and cursor key mode (DECCKM) configuration - Filtering DA responses prevented htop from detecting terminal type, causing arrow keys to be misinterpreted - The original Neovim issue (DA responses showing on screen) was likely caused by specific PTY configurations and should be handled there - Update tests: test_da_response_filtered → test_da_response_passthrough
- Change arrow key sequences from CSI format (\x1b[A) to SS3 format (\x1bOA) - ncurses applications (htop) expect SS3 format when DECCKM is enabled - vim/neovim and most terminals accept both formats, but ncurses strictly follows terminfo which typically specifies SS3 format for cursor keys - This fixes arrow key navigation in htop while maintaining compatibility with other applications
- Add #[serial] to tests that modify SSH_AUTH_SOCK and HOME env vars - Properly save/restore original env vars in test_determine_auth_method_with_agent - Prevents race conditions when tests run in parallel in CI
…arning - Update num-bigint-dig from 0.8.4 to 0.8.6 via cargo update - Fixes Rust future incompatibility warning about private `vec` macro - See: rust-lang/rust#120192
Summary
When running terminal applications like Neovim via SSH, raw terminal capability query responses (XTGETTCAP, DA1/DA2/DA3, OSC) were appearing as visible text on screen. This PR fixes the issue by:
Changes
src/pty/session/escape_filter.rs- State machine-based filter for terminal response sequencessrc/pty/session/session_manager.rs- Added TERM env var propagation and escape filteringsrc/pty/session/mod.rs- Added escape_filter moduleARCHITECTURE.md- Documented the terminal escape sequence filtering designFiltered Sequences
The filter handles the following types of terminal responses:
\x1bP+r...): Terminal capability query responses\x1b[?...c): Device Attributes responses\x1b]...): Operating System Command responses (colors, clipboard)\x1bP...): Device Control String responsesTest Plan
nvimafter SSH connection via bsshFixes #76