security: bypass proxy for loopback and suppress HTTP wire credential logging (F-003, F-005)

This commit is contained in:
Dylan M. Taylor
2026-10-24 21:58:20 -04:00
parent c0fa1c69f4
commit 004fe617af
4 changed files with 64 additions and 15 deletions
+28 -2
View File
@@ -119,6 +119,21 @@ impl Sink {
}
}
/// Suppresses sensitive authorization tokens in log output (CWE-532).
pub(crate) fn redact_credentials(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut remaining = text;
while let Some(idx) = remaining.to_ascii_lowercase().find("bearer ") {
result.push_str(&remaining[..idx + 7]);
let token_rest = &remaining[idx + 7..];
let end_token = token_rest.find(|c: char| c.is_whitespace() || c == '"' || c == '\'' || c == '\r' || c == '\n').unwrap_or(token_rest.len());
result.push_str("[REDACTED]");
remaining = &token_rest[end_token..];
}
result.push_str(remaining);
result
}
/// env_logger's output: each formatted record to standard error and to the [`Sink`].
struct Tee {
sink: Arc<Mutex<Sink>>,
@@ -127,10 +142,12 @@ struct Tee {
impl Write for Tee {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let text = String::from_utf8_lossy(buf);
let sanitized = redact_credentials(&text);
if self.stderr {
let _ = std::io::stderr().write_all(buf);
let _ = std::io::stderr().write_all(sanitized.as_bytes());
}
self.sink.lock().unwrap_or_else(PoisonError::into_inner).write(&String::from_utf8_lossy(buf));
self.sink.lock().unwrap_or_else(PoisonError::into_inner).write(&sanitized);
Ok(buf.len())
}
@@ -164,6 +181,8 @@ impl Logs {
/// An env_logger builder (filter from `RUST_LOG`, else [`DEFAULT_FILTER`]) writing through a [`Tee`].
fn builder(sink: Arc<Mutex<Sink>>, stderr: bool) -> env_logger::Builder {
let mut b = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(DEFAULT_FILTER));
// Suppress raw HTTP wire TRACE records before formatting and output, regardless of RUST_LOG (CWE-532).
b.filter_module("ureq_proto", log::LevelFilter::Debug);
b.target(env_logger::Target::Pipe(Box::new(Tee { sink, stderr })));
b
}
@@ -329,4 +348,11 @@ mod tests {
assert!(read(&path).contains("after the panic\n"));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn redact_credentials_replaces_bearer_tokens() {
assert_eq!(redact_credentials("Authorization: Bearer ghp_secretToken1234567890"), "Authorization: Bearer [REDACTED]");
assert_eq!(redact_credentials("header \"authorization: bearer my_secret\" and more"), "header \"authorization: bearer [REDACTED]\" and more");
assert_eq!(redact_credentials("normal log line without tokens"), "normal log line without tokens");
}
}