Skip to content

Commit 31166f7

Browse files
committed
Remove full CSI codes
1 parent 14b7330 commit 31166f7

File tree

2 files changed

+27
-10
lines changed

2 files changed

+27
-10
lines changed

tracing-subscriber/src/fmt/format/escape.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ impl<T: fmt::Debug> fmt::Debug for Escape<T> {
1010
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1111
let mut escaping_writer = EscapingWriter {
1212
inner: f,
13-
skip_control_chars: false,
13+
skip_csi_codes: false,
1414
};
1515
write!(escaping_writer, "{:?}", self.0)
1616
}
@@ -24,7 +24,7 @@ impl<T: fmt::Debug> fmt::Debug for EscapeSkip<T> {
2424
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2525
let mut escaping_writer = EscapingWriter {
2626
inner: f,
27-
skip_control_chars: true,
27+
skip_csi_codes: true,
2828
};
2929
write!(escaping_writer, "{:?}", self.0)
3030
}
@@ -33,21 +33,32 @@ impl<T: fmt::Debug> fmt::Debug for EscapeSkip<T> {
3333
/// Helper struct that escapes ANSI sequences as characters are written
3434
struct EscapingWriter<'a, 'b> {
3535
inner: &'a mut fmt::Formatter<'b>,
36-
skip_control_chars: bool,
36+
skip_csi_codes: bool,
3737
}
3838

3939
impl<'a, 'b> fmt::Write for EscapingWriter<'a, 'b> {
4040
fn write_str(&mut self, s: &str) -> fmt::Result {
4141
// Stream the string character by character, escaping all control sequences
42-
for ch in s.chars() {
42+
let mut chars = s.chars().peekable();
43+
while let Some(ch) = chars.next() {
44+
// Recognize and remove ECMA-48 CSI codes
45+
// This is a best effort to clean up colour codes in the message field
46+
if ch == '\x1b' && self.skip_csi_codes {
47+
if chars.next_if_eq(&'[').is_some() {
48+
// Remove parameter and intermediate bytes
49+
while chars.next_if(|x| matches!(x, '\x20'..='\x3f')).is_some() {}
50+
// Remove final byte
51+
chars.next_if(|x| matches!(x, '\x40'..='\x7E'));
52+
continue;
53+
}
54+
}
55+
4356
// ESC BEL BS FF DEL
4457
if matches!(ch, '\x1b' | '\x07' | '\x08' | '\x0c' | '\x7f'..='\u{9f}') {
45-
if !self.skip_control_chars {
46-
if ch.is_ascii() {
47-
write!(self.inner, "\\x{:02x}", ch as u32)?
48-
} else {
49-
write!(self.inner, "\\u{{{:x}}}", ch as u32)?
50-
}
58+
if ch.is_ascii() {
59+
write!(self.inner, "\\x{:02x}", ch as u32)?
60+
} else {
61+
write!(self.inner, "\\u{{{:x}}}", ch as u32)?
5162
}
5263
} else {
5364
self.inner.write_char(ch)?;

tracing-subscriber/tests/ansi_escaping.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,10 +160,12 @@ fn test_pretty_ansi_escaping() {
160160

161161
tracing::subscriber::with_default(subscriber, || {
162162
let malicious_input = "\x1b]0;PWNED\x07\x1b[2J";
163+
let colourful = "[\u{1b}[1m\u{1b}[38;5;9merror\u{1b}[0m\u{1b}[1m: Invalid JSON\u{1b}[0m]";
163164

164165
// Pretty formatter should escape ANSI sequences
165166
tracing::info!("Testing: {}", malicious_input);
166167
tracing::info!(user_input = %malicious_input, "Field test");
168+
tracing::info!("Colour test: {}", colourful);
167169
});
168170

169171
let output = writer.get_output();
@@ -177,6 +179,10 @@ fn test_pretty_ansi_escaping() {
177179
!output.contains('\x07'),
178180
"Pretty output should not contain raw BEL characters"
179181
);
182+
assert!(
183+
output.contains("[error: Invalid JSON]"),
184+
"ansi escape code should be removed from message entirely"
185+
)
180186
}
181187

182188
/// Comprehensive test for ANSI sanitization that prevents injection attacks

0 commit comments

Comments
 (0)