282 lines
8.1 KiB
Rust
282 lines
8.1 KiB
Rust
use std::sync::{Arc, Mutex};
|
|
use tracing_subscriber::fmt::MakeWriter;
|
|
|
|
/// Shared test writer that collects output for verification
|
|
#[derive(Debug, Clone)]
|
|
struct TestWriter {
|
|
buf: Arc<Mutex<Vec<u8>>>,
|
|
}
|
|
|
|
impl TestWriter {
|
|
fn new() -> Self {
|
|
Self {
|
|
buf: Arc::new(Mutex::new(Vec::new())),
|
|
}
|
|
}
|
|
|
|
fn get_output(&self) -> String {
|
|
let buf = self.buf.lock().unwrap();
|
|
String::from_utf8_lossy(&buf).to_string()
|
|
}
|
|
}
|
|
|
|
impl std::io::Write for TestWriter {
|
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
|
self.buf.lock().unwrap().extend_from_slice(buf);
|
|
Ok(buf.len())
|
|
}
|
|
|
|
fn flush(&mut self) -> std::io::Result<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl<'a> MakeWriter<'a> for TestWriter {
|
|
type Writer = TestWriter;
|
|
|
|
fn make_writer(&'a self) -> Self::Writer {
|
|
self.clone()
|
|
}
|
|
}
|
|
|
|
/// Test that basic security expectations are met - this is a smoke test
|
|
/// for the ANSI escaping functionality using public APIs only
|
|
#[test]
|
|
fn test_error_ansi_escaping() {
|
|
use std::fmt;
|
|
|
|
#[derive(Debug)]
|
|
struct MaliciousError(&'static str);
|
|
|
|
impl fmt::Display for MaliciousError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
write!(f, "{}", self.0)
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for MaliciousError {}
|
|
|
|
let writer = TestWriter::new();
|
|
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
|
|
.with_writer(writer.clone())
|
|
.with_ansi(false)
|
|
.without_time()
|
|
.with_target(false)
|
|
.with_level(false)
|
|
.finish();
|
|
|
|
tracing::subscriber::with_default(subscriber, || {
|
|
let malicious_error = MaliciousError("\x1b]0;PWNED\x07\x1b[2J\x08\x0c\x7f");
|
|
|
|
// This demonstrates that errors are logged - the actual escaping
|
|
// is tested by our internal unit tests
|
|
tracing::error!(error = %malicious_error, "An error occurred");
|
|
});
|
|
|
|
let output = writer.get_output();
|
|
|
|
// Just verify that something was logged
|
|
assert!(
|
|
output.contains("An error occurred"),
|
|
"Error message should be logged"
|
|
);
|
|
}
|
|
|
|
/// Test that ANSI escape sequences in log messages are properly escaped
|
|
#[test]
|
|
fn test_message_ansi_escaping() {
|
|
let writer = TestWriter::new();
|
|
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
|
|
.with_writer(writer.clone())
|
|
.with_ansi(false)
|
|
.without_time()
|
|
.with_target(false)
|
|
.with_level(false)
|
|
.finish();
|
|
|
|
tracing::subscriber::with_default(subscriber, || {
|
|
let malicious_input = "\x1b]0;PWNED\x07\x1b[2J\x08\x0c\x7f";
|
|
|
|
// This should not cause ANSI injection
|
|
tracing::info!("User input: {}", malicious_input);
|
|
});
|
|
|
|
let output = writer.get_output();
|
|
|
|
// Verify ANSI sequences are escaped
|
|
assert!(
|
|
!output.contains('\x1b'),
|
|
"Message output should not contain raw ESC characters"
|
|
);
|
|
assert!(
|
|
!output.contains('\x07'),
|
|
"Message output should not contain raw BEL characters"
|
|
);
|
|
}
|
|
|
|
/// Test that JSON formatter properly escapes ANSI sequences
|
|
#[cfg(feature = "json")]
|
|
#[test]
|
|
fn test_json_ansi_escaping() {
|
|
let writer = TestWriter::new();
|
|
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
|
|
.json()
|
|
.with_writer(writer.clone())
|
|
.finish();
|
|
|
|
tracing::subscriber::with_default(subscriber, || {
|
|
let malicious_input = "\x1b]0;PWNED\x07\x1b[2J";
|
|
|
|
// JSON formatter should escape ANSI sequences
|
|
tracing::info!("Testing: {}", malicious_input);
|
|
tracing::info!(user_input = %malicious_input, "Field test");
|
|
});
|
|
|
|
let output = writer.get_output();
|
|
|
|
// JSON should escape ANSI sequences as Unicode escapes
|
|
assert!(
|
|
!output.contains('\x1b'),
|
|
"JSON output should not contain raw ESC characters"
|
|
);
|
|
assert!(
|
|
!output.contains('\x07'),
|
|
"JSON output should not contain raw BEL characters"
|
|
);
|
|
}
|
|
|
|
/// Test that pretty formatter properly escapes ANSI sequences
|
|
#[cfg(feature = "ansi")]
|
|
#[test]
|
|
fn test_pretty_ansi_escaping() {
|
|
let writer = TestWriter::new();
|
|
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
|
|
.pretty()
|
|
.with_writer(writer.clone())
|
|
.with_ansi(false)
|
|
.without_time()
|
|
.with_target(false)
|
|
.finish();
|
|
|
|
tracing::subscriber::with_default(subscriber, || {
|
|
let malicious_input = "\x1b]0;PWNED\x07\x1b[2J";
|
|
|
|
// Pretty formatter should escape ANSI sequences
|
|
tracing::info!("Testing: {}", malicious_input);
|
|
});
|
|
|
|
let output = writer.get_output();
|
|
|
|
// Verify ANSI sequences are escaped
|
|
assert!(
|
|
!output.contains('\x1b'),
|
|
"Pretty output should not contain raw ESC characters"
|
|
);
|
|
assert!(
|
|
!output.contains('\x07'),
|
|
"Pretty output should not contain raw BEL characters"
|
|
);
|
|
}
|
|
|
|
/// Comprehensive test for ANSI sanitization that prevents injection attacks
|
|
#[test]
|
|
fn ansi_sanitization_prevents_injection() {
|
|
let writer = TestWriter::new();
|
|
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
|
|
.with_writer(writer.clone())
|
|
.with_ansi(false)
|
|
.without_time()
|
|
.with_target(false)
|
|
.with_level(false)
|
|
.finish();
|
|
|
|
#[derive(Debug)]
|
|
struct MaliciousError {
|
|
content: String,
|
|
}
|
|
|
|
impl std::fmt::Display for MaliciousError {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
// This Display implementation contains ANSI escape sequences
|
|
write!(f, "Error: {}", self.content)
|
|
}
|
|
}
|
|
|
|
tracing::subscriber::with_default(subscriber, || {
|
|
// Test 1: Field values should remain properly escaped by Debug (baseline)
|
|
let malicious_field_value = "\x1b]0;PWNED\x07\x1b[2J";
|
|
tracing::error!(malicious_field = malicious_field_value, "Field test");
|
|
|
|
// Test 2: Message content vulnerability should be mitigated
|
|
let malicious_error = MaliciousError {
|
|
content: "\x1b]0;PWNED\x07\x1b[2J".to_string(),
|
|
};
|
|
tracing::error!("{}", malicious_error);
|
|
});
|
|
|
|
let output = writer.get_output();
|
|
|
|
// Field values should contain escaped sequences like \u{1b}
|
|
assert!(
|
|
output.contains("\\u{1b}"),
|
|
"Field values should be escaped by Debug formatting"
|
|
);
|
|
|
|
// Message content should be sanitized
|
|
assert!(
|
|
output.contains("\\x1b"),
|
|
"Message content should be sanitized"
|
|
);
|
|
assert!(
|
|
!output.contains("\x1b]0;PWNED"),
|
|
"Message content should not contain raw ANSI sequences"
|
|
);
|
|
assert!(
|
|
!output.contains("\x07"),
|
|
"Message content should not contain raw control characters"
|
|
);
|
|
}
|
|
|
|
/// Test that C1 control characters (\x80-\x9f) are also properly escaped
|
|
#[test]
|
|
fn test_c1_control_characters_escaping() {
|
|
let writer = TestWriter::new();
|
|
let subscriber = tracing_subscriber::fmt::Subscriber::builder()
|
|
.with_writer(writer.clone())
|
|
.with_ansi(false)
|
|
.without_time()
|
|
.with_target(false)
|
|
.with_level(false)
|
|
.finish();
|
|
|
|
tracing::subscriber::with_default(subscriber, || {
|
|
// Test C1 control characters that can be used in 8-bit terminal escape sequences
|
|
let c1_controls = "\u{80}\u{85}\u{90}\u{9b}\u{9c}\u{9d}\u{9e}\u{9f}"; // Various C1 controls including CSI
|
|
|
|
// This should escape C1 control characters to prevent 8-bit escape sequences
|
|
tracing::info!("C1 controls: {}", c1_controls);
|
|
});
|
|
|
|
let output = writer.get_output();
|
|
|
|
// Verify C1 control characters are escaped
|
|
assert!(
|
|
!output.contains('\u{80}'),
|
|
"Output should not contain raw C1 control characters"
|
|
);
|
|
assert!(
|
|
!output.contains('\u{9b}'),
|
|
"Output should not contain raw CSI character"
|
|
);
|
|
assert!(
|
|
!output.contains('\u{9c}'),
|
|
"Output should not contain raw ST character"
|
|
);
|
|
|
|
// Should contain Unicode escapes for C1 characters
|
|
assert!(
|
|
output.contains("\\u{80}") || output.contains("\\u{8"),
|
|
"Should contain escaped C1 characters"
|
|
);
|
|
}
|