From 9e294e3e9fe8f4b1555daa94ca2e2116730193c4 Mon Sep 17 00:00:00 2001 From: carfeii <277509339+carfeii@users.noreply.github.com> Date: Thu, 6 Aug 2026 14:31:46 +0800 Subject: [PATCH] Strip terminal escape sequences from the Command column Command::add read each process's command line and only replaced newline and tab characters before displaying it, with no handling of other control characters or ANSI/terminal escape sequences. Since a process's command line (including argv[0]) is fully controlled by whichever user started it, and /proc//cmdline is world-readable on Linux regardless of process ownership, any local unprivileged user could plant a process whose command line contains a raw escape sequence, which would then be printed unmodified to any other user's terminal when they run `procs`, a routine action that displays every visible process. Add util::sanitize_display, which replaces control characters (including ESC) with the Unicode replacement character, and apply it to the final command-line string in all four platform variants of Command::add (Linux/Android, macOS, Windows, FreeBSD). See dalance/procs#950. --- src/columns/command.rs | 6 ++++-- src/util.rs | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/columns/command.rs b/src/columns/command.rs index 1add31432..38b80ca22 100644 --- a/src/columns/command.rs +++ b/src/columns/command.rs @@ -47,6 +47,7 @@ impl Column for Command { } else { proc.curr_proc.stat().comm.clone() }; + let fmt_content = crate::util::sanitize_display(&fmt_content); let raw_content = fmt_content.clone(); self.fmt_contents.insert(proc.pid, fmt_content); @@ -79,6 +80,7 @@ impl Column for Command { } else { String::from("") }; + let fmt_content = crate::util::sanitize_display(&fmt_content); let raw_content = fmt_content.clone(); self.fmt_contents.insert(proc.pid, fmt_content); @@ -91,7 +93,7 @@ impl Column for Command { #[cfg(target_os = "windows")] impl Column for Command { fn add(&mut self, proc: &ProcessInfo) { - let fmt_content = proc.command.clone(); + let fmt_content = crate::util::sanitize_display(&proc.command); let raw_content = fmt_content.clone(); self.fmt_contents.insert(proc.pid, fmt_content); @@ -119,7 +121,7 @@ impl Column for Command { } x }; - let fmt_content = command; + let fmt_content = crate::util::sanitize_display(&command); let raw_content = fmt_content.clone(); self.fmt_contents.insert(proc.pid, fmt_content); diff --git a/src/util.rs b/src/util.rs index 3688d92c7..e0197df56 100644 --- a/src/util.rs +++ b/src/util.rs @@ -223,6 +223,19 @@ pub fn truncate(s: &'_ str, width: usize) -> Cow<'_, str> { } } +/// Replace control characters, including ANSI/terminal escape sequences, +/// with the Unicode replacement character. Process-derived content (such +/// as a command line) is fully controlled by whichever user started the +/// process, so it must be sanitized before being written to the +/// terminal, since an unprivileged local user could otherwise plant an +/// escape sequence that gets interpreted by any other user's terminal +/// when they view the process listing. +pub fn sanitize_display(s: &str) -> String { + s.chars() + .map(|c| if c.is_control() { '\u{FFFD}' } else { c }) + .collect() +} + /// Trim trailing whitespace from a string that may contain ANSI escape sequences. /// Unlike str::trim_end(), this correctly handles ANSI codes at the end of the string /// that would otherwise prevent trimming of trailing whitespace. @@ -383,3 +396,27 @@ pub fn process_new( procfs::process::Process::new(pid) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sanitize_display_strips_escape_sequences() { + // A crafted argv0 embedding a raw OSC 52 (clipboard-write) escape + // sequence, the same shape a malicious local process could use to + // inject terminal control sequences into another user's `procs` + // output. The ESC and BEL bytes must not survive sanitization. + let malicious = "\x1b]52;c;cGF5bG9hZA==\x07innocuous_process"; + let sanitized = sanitize_display(malicious); + assert!(!sanitized.contains('\u{1b}')); + assert!(!sanitized.contains('\u{7}')); + assert!(sanitized.contains("innocuous_process")); + } + + #[test] + fn sanitize_display_preserves_normal_text() { + let normal = "/usr/bin/env python3 script.py --flag value"; + assert_eq!(sanitize_display(normal), normal); + } +}