Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions tracing-subscriber/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# 0.3.20 (August 29, 2025)

[ [crates.io][crate-0.3.20] ] | [ [docs.rs][docs-0.3.20] ]

### Fixed

- Escape ANSI escape sequences in logs

# 0.3.19 (November 29, 2024)

[ [crates.io][crate-0.3.19] ] | [ [docs.rs][docs-0.3.19] ]
Expand Down
2 changes: 1 addition & 1 deletion tracing-subscriber/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "tracing-subscriber"
version = "0.3.19"
version = "0.3.20"
authors = [
"Eliza Weisman <eliza@buoyant.io>",
"David Barsky <me@davidbarsky.com>",
Expand Down
51 changes: 51 additions & 0 deletions tracing-subscriber/src/fmt/format/escape.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//! ANSI escape sequence sanitization to prevent terminal injection attacks.

use std::fmt::{self, Write};

/// A wrapper that implements `fmt::Debug` and `fmt::Display` and escapes ANSI sequences on-the-fly.
/// This avoids creating intermediate strings while providing security against terminal injection.
pub(super) struct Escape<T>(pub(super) T);

/// Helper struct that escapes ANSI sequences as characters are written
struct EscapingWriter<'a, 'b> {
inner: &'a mut fmt::Formatter<'b>,
}

impl<'a, 'b> fmt::Write for EscapingWriter<'a, 'b> {
fn write_str(&mut self, s: &str) -> fmt::Result {
// Stream the string character by character, escaping ANSI and C1 control sequences
for ch in s.chars() {
match ch {
// C0 control characters that can be used in terminal escape sequences
'\x1b' => self.inner.write_str("\\x1b")?, // ESC
'\x07' => self.inner.write_str("\\x07")?, // BEL
'\x08' => self.inner.write_str("\\x08")?, // BS
'\x0c' => self.inner.write_str("\\x0c")?, // FF
'\x7f' => self.inner.write_str("\\x7f")?, // DEL

// C1 control characters (\x80-\x9f) - 8-bit control codes
// These can be used as alternative escape sequences in some terminals
ch if ch as u32 >= 0x80 && ch as u32 <= 0x9f => {
write!(self.inner, "\\u{{{:x}}}", ch as u32)?
},

_ => self.inner.write_char(ch)?,
}
}
Ok(())
}
}

impl<T: fmt::Debug> fmt::Debug for Escape<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut escaping_writer = EscapingWriter { inner: f };
write!(escaping_writer, "{:?}", self.0)
}
}

impl<T: fmt::Display> fmt::Display for Escape<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut escaping_writer = EscapingWriter { inner: f };
write!(escaping_writer, "{}", self.0)
}
}
15 changes: 11 additions & 4 deletions tracing-subscriber/src/fmt/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ use tracing_log::NormalizeEvent;
#[cfg(feature = "ansi")]
use nu_ansi_term::{Color, Style};


mod escape;
use escape::Escape;

#[cfg(feature = "json")]
mod json;
#[cfg(feature = "json")]
Expand Down Expand Up @@ -1257,15 +1261,15 @@ impl field::Visit for DefaultVisitor<'_> {
field,
&format_args!(
"{} {}{}{}{}",
value,
Escape(&format_args!("{}", value)),
italic.paint(field.name()),
italic.paint(".sources"),
self.writer.dimmed().paint("="),
ErrorSourceList(source)
),
)
} else {
self.record_debug(field, &format_args!("{}", value))
self.record_debug(field, &format_args!("{}", Escape(&format_args!("{}", value))))
}
}

Expand All @@ -1287,7 +1291,10 @@ impl field::Visit for DefaultVisitor<'_> {
self.maybe_pad();

self.result = match name {
"message" => write!(self.writer, "{:?}", value),
"message" => {
// Escape ANSI characters to prevent malicious patterns (e.g., terminal injection attacks)
write!(self.writer, "{:?}", Escape(value))
},
name if name.starts_with("r#") => write!(
self.writer,
"{}{}{:?}",
Expand Down Expand Up @@ -1326,7 +1333,7 @@ impl Display for ErrorSourceList<'_> {
let mut list = f.debug_list();
let mut curr = Some(self.0);
while let Some(curr_err) = curr {
list.entry(&format_args!("{}", curr_err));
list.entry(&Escape(&format_args!("{}", curr_err)));
curr = curr_err.source();
}
list.finish()
Expand Down
9 changes: 6 additions & 3 deletions tracing-subscriber/src/fmt/format/pretty.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,15 +457,15 @@ impl field::Visit for PrettyVisitor<'_> {
field,
&format_args!(
"{}, {}{}.sources{}: {}",
value,
Escape(&format_args!("{}", value)),
bold.prefix(),
field,
bold.infix(self.style),
ErrorSourceList(source),
),
)
} else {
self.record_debug(field, &format_args!("{}", value))
self.record_debug(field, &Escape(&format_args!("{}", value)))
}
}

Expand All @@ -475,7 +475,10 @@ impl field::Visit for PrettyVisitor<'_> {
}
let bold = self.bold();
match field.name() {
"message" => self.write_padded(&format_args!("{}{:?}", self.style.prefix(), value,)),
"message" => {
// Escape ANSI characters to prevent malicious patterns (e.g., terminal injection attacks)
self.write_padded(&format_args!("{}{:?}", self.style.prefix(), Escape(value)))
},
// Skip fields that are actually log metadata that have already been handled
#[cfg(feature = "tracing-log")]
name if name.starts_with("log.") => self.result = Ok(()),
Expand Down
Loading
Loading