From f591f76d6ed782974e89c1a0b5d18070e3f2bddb Mon Sep 17 00:00:00 2001 From: Randall Naar Date: Mon, 24 Aug 2026 16:26:36 -0400 Subject: [PATCH] Redacted RPC password from config logs. --- src/config.rs | 58 ++++++++++++++++++++++++++++++++++++-- tests/config.rs | 74 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 tests/config.rs diff --git a/src/config.rs b/src/config.rs index 341e3fb2b..90cf113ad 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,6 @@ use clap::{App, Arg}; use dirs::home_dir; +use std::fmt; use std::fs; use std::net::SocketAddr; use std::net::ToSocketAddrs; @@ -17,6 +18,33 @@ use bitcoin::Network as BNetwork; const ELECTRS_VERSION: &str = env!("CARGO_PKG_VERSION"); +#[derive(Clone)] +pub struct SensitiveAuth(String); + +impl SensitiveAuth { + pub fn new(value: String) -> Self { + Self(value) + } + + fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +impl fmt::Debug for SensitiveAuth { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let username = self + .0 + .split_once(':') + .map(|(username, _)| username) + .unwrap_or(""); + f.debug_tuple("UserPass") + .field(&username) + .field(&"") + .finish() + } +} + #[derive(Debug, Clone)] pub struct Config { // See below for the documentation of each field: @@ -29,7 +57,7 @@ pub struct Config { pub daemon_rpc_fallback_addr: Option, pub daemon_parallelism: usize, pub daemon_conn_max_age: Option, - pub cookie: Option, + pub cookie: Option, pub electrum_rpc_addr: SocketAddr, pub electrum_rpc_conn_max_age: Option, pub http_addr: SocketAddr, @@ -496,7 +524,9 @@ impl Config { .value_of("blocks_dir") .map(PathBuf::from) .unwrap_or_else(|| daemon_dir.join("blocks")); - let cookie = m.value_of("cookie").map(|s| s.to_owned()); + let cookie = m + .value_of("cookie") + .map(|s| SensitiveAuth::new(s.to_owned())); let electrum_banner = m.value_of("electrum_banner").map_or_else( || format!("Welcome to electrs-esplora {}", ELECTRS_VERSION), @@ -573,7 +603,14 @@ impl Config { #[cfg(feature = "electrum-discovery")] tor_proxy: m.value_of("tor_proxy").map(|s| s.parse().unwrap()), }; - eprintln!("{:?}", config); + match &config.cookie { + Some(auth) => log::debug!("daemon authentication: {:?}", auth), + None => log::debug!( + "daemon authentication: CookieFile({:?})", + config.daemon_dir.join(".cookie") + ), + } + log::debug!("configuration: {:?}", config); config } @@ -650,3 +687,18 @@ impl CookieGetter for CookieFile { Ok(contents) } } + +#[cfg(test)] +mod tests { + use super::SensitiveAuth; + + #[test] + fn sensitive_auth_debug_redacts_password() { + let password = "poc-PASSWORD-123"; + let auth = SensitiveAuth::new(format!("poc-user:{}", password)); + let rendered = format!("{:?}", auth); + + assert_eq!(rendered, r#"UserPass("poc-user", "")"#); + assert!(!rendered.contains(password)); + } +} diff --git a/tests/config.rs b/tests/config.rs new file mode 100644 index 000000000..4120d00b3 --- /dev/null +++ b/tests/config.rs @@ -0,0 +1,74 @@ +use std::net::TcpListener; +use std::path::Path; +use std::process::{Command, Output}; + +fn run_electrs(temp_dir: &Path, extra_args: &[&str]) -> Output { + let monitoring_listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let monitoring_addr = monitoring_listener.local_addr().unwrap().to_string(); + + Command::new(env!("CARGO_BIN_EXE_electrs")) + .args([ + "--db-dir", + temp_dir.join("db").to_str().unwrap(), + "--daemon-dir", + temp_dir.to_str().unwrap(), + "--daemon-rpc-addr", + "127.0.0.1:1", + "--monitoring-addr", + monitoring_addr.as_str(), + ]) + .args(extra_args) + .output() + .expect("failed to run electrs") +} + +#[test] +fn startup_never_logs_static_auth_password() { + let password = "poc-PASSWORD-123"; + let cookie = format!("poc-user:{}", password); + + for verbosity in [None, Some("-v"), Some("-vv")] { + let temp_dir = tempfile::tempdir().unwrap(); + let mut extra_args = vec!["--cookie", cookie.as_str()]; + if let Some(verbosity) = verbosity { + extra_args.push(verbosity); + } + + let output = run_electrs(temp_dir.path(), &extra_args); + let stderr = String::from_utf8(output.stderr).unwrap(); + + assert!(!output.status.success(), "electrs unexpectedly succeeded"); + assert!( + !stderr.contains(password), + "password was logged at verbosity {:?}: {}", + verbosity, + stderr + ); + + if verbosity.is_some() { + assert!( + stderr.contains(r#"daemon authentication: UserPass("poc-user", "")"#), + "redacted authentication mode missing from stderr: {}", + stderr + ); + } + } +} + +#[test] +fn startup_debug_log_identifies_cookie_file() { + let temp_dir = tempfile::tempdir().unwrap(); + let output = run_electrs(temp_dir.path(), &["-v"]); + let stderr = String::from_utf8(output.stderr).unwrap(); + let expected = format!( + "daemon authentication: CookieFile({:?})", + temp_dir.path().join(".cookie") + ); + + assert!(!output.status.success(), "electrs unexpectedly succeeded"); + assert!( + stderr.contains(&expected), + "cookie-file authentication mode missing from stderr: {}", + stderr + ); +}