From aba9f065b3da920bb265b02f99aff1fb00de2437 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 9 Sep 2026 10:26:10 +0200 Subject: [PATCH 1/2] fix: Escape control characters when built without `color` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: Dropping the `color` feature removes the `anstream` layer, and with it all ANSI stripping — logging untrusted input then writes escape sequences verbatim into the output. Stored logs are the worse case: `cat`, `tail -f` and `grep` replay them raw at every future read, so OSC 52 clipboard writes, title-setting and cursor motion keep firing long after the fact. The build cannot just keep `color` to get sanitized: with styling disabled `anstream` still runs its VT state machine over every byte, which triples per-record cost. Summary of Changes: - escape C0 and DEL as `\xNN` in `print` when `color` is off - spare `\n` and `\t`, which the record format itself relies on - borrow via `Cow` so records without controls copy nothing --- src/writer/buffer.rs | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/writer/buffer.rs b/src/writer/buffer.rs index f55ac4d..9600c2a 100644 --- a/src/writer/buffer.rs +++ b/src/writer/buffer.rs @@ -56,6 +56,8 @@ impl BufferWriter { use std::io::Write as _; let buf = buf.as_bytes(); + #[cfg(not(feature = "color"))] + let buf = &escape_controls(buf); match &self.target { WritableTarget::WriteStdout => { let stream = io::stdout(); @@ -104,6 +106,44 @@ impl BufferWriter { } } +/// Escape control characters as `\xNN`, so that logging untrusted input can't +/// drive the terminal of whoever later reads the output. This build emits no +/// styling of its own, so unlike `anstream` it needs no VT parser to do it. +#[cfg(not(feature = "color"))] +fn escape_controls(buf: &[u8]) -> std::borrow::Cow<'_, [u8]> { + const HEX: &[u8; 16] = b"0123456789abcdef"; + + // Bitwise fold rather than `any`: short-circuiting would force a scalar loop. + let has_control = buf.iter().fold(false, |acc, &b| acc | is_control(b)); + if !has_control { + return std::borrow::Cow::Borrowed(buf); + } + + // Only records that actually carry controls pay for a copy, and the scan + // above found at least one, each of which grows the record by 3 bytes. + let mut escaped = Vec::with_capacity(buf.len() + 3); + for &byte in buf { + if is_control(byte) { + let (hi, lo) = (usize::from(byte >> 4), usize::from(byte & 0xf)); + escaped.extend_from_slice(&[b'\\', b'x', HEX[hi], HEX[lo]]); + } else { + escaped.push(byte); + } + } + std::borrow::Cow::Owned(escaped) +} + +/// Control is all of C0 plus DEL, minus `\n` and `\t`, which the format itself +/// relies on. Stricter than `anstream`, whose strip passes `\r`, VT and FF. +/// +/// Multi-byte UTF-8 isn't flagged, having no byte in the tested ASCII range, +/// nor is raw C1; records come from `str`, so only a custom format emits that. +#[cfg(not(feature = "color"))] +#[inline] +fn is_control(byte: u8) -> bool { + ((byte < 0x20) & (byte != b'\n') & (byte != b'\t')) | (byte == 0x7f) +} + #[cfg(feature = "color")] fn adapt(buf: &[u8], write_style: WriteStyle) -> io::Result> { use std::io::Write as _; From a459fbd57cbe6e2f2bb4e2e2b27ffefc62b2e162 Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Wed, 9 Sep 2026 10:39:34 +0200 Subject: [PATCH 2/2] docs: Describe control character escaping without `color` Problem: The colors warning still tells readers that dropping the `color` feature leaves them unprotected, which no longer holds now that such builds escape control characters themselves. Summary of Changes: - list dropping `color` among the mitigations, noting it escapes rather than strips - call out that `\n` stays unescaped in every configuration --- src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7770bf1..b75d858 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -218,9 +218,11 @@ //! - In the application, calling [`Builder::write_style(Never)`][Builder::write_style] to have all ANSI escape codes stripped //! - In the application, [stripping ANSI escape codes](https://docs.rs/anstream/latest/anstream/adapter/fn.strip_str.html) //! from user inputs +//! - Deactivating the build-time feature `color`, which escapes control characters as `\xNN` +//! rather than stripping them, and also covers the `\r`, VT and FF that stripping passes on //! -//! Note: deactivating the build-time feature `color` is not a mitigation as that removes all ANSI escape code -//! stripping from `env_logger`. +//! Note: `\n` is never escaped, so untrusted input can still read as an extra record. The +//! default [indentation][Builder::format_indent] offsets such lines by four spaces. //! //! //!