From eef8c524970da2edf7c6fd9d13395f0d39abc117 Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Thu, 16 Jul 2026 04:31:10 -0700 Subject: [PATCH] =?UTF-8?q?fix(official-sync):=20=E8=84=B1=E6=95=8F?= =?UTF-8?q?=E4=BB=A3=E7=90=86=E5=87=AD=E6=8D=AE=E9=81=BF=E5=85=8D=E6=B3=84?= =?UTF-8?q?=E6=BC=8F=E5=88=B0=E6=97=A5=E5=BF=97=E4=B8=8E=E7=8A=B6=E6=80=81?= =?UTF-8?q?=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两处代理凭据泄漏: - redact_proxy_url 对无 scheme 的代理串(user:secret@host:port,curl 默认按 http 处理)完全不脱敏,明文凭据会进入 progress 事件、daemon 日志和 bat-events.jsonl。现对无 scheme 但含 userinfo 的情况一并脱敏。 - daemon 保存的 command 含原始代理 URL,经 RPC status 和 bat status --json 回显到 stdout(会被监控采集)。新增 redact_command_proxy_credentials,在 build_daemon_status_report 的对外拷贝里脱敏 --proxy 值;磁盘状态文件 (0600)仍保留原始 command 供 restart/reload 复用。 残留:子进程 argv 仍可经 /proc//cmdline 读到代理凭据,以及 0600 状态 文件内的 at-rest 凭据,二者受进程属主与文件权限约束,后续可改由环境变量下 传子进程进一步收敛。 对应 issue #18 维护清单 1-2。 Co-Authored-By: Claude Fable 5 --- infrastructure/src/bin/bat_official_sync.rs | 70 ++++++++++++++++++++- infrastructure/src/curl_transfer.rs | 34 ++++++++-- 2 files changed, 97 insertions(+), 7 deletions(-) diff --git a/infrastructure/src/bin/bat_official_sync.rs b/infrastructure/src/bin/bat_official_sync.rs index 4b07af0..825fe8e 100644 --- a/infrastructure/src/bin/bat_official_sync.rs +++ b/infrastructure/src/bin/bat_official_sync.rs @@ -1,6 +1,6 @@ use bat_adapters::official::yostar_jp::PatchPlatform; use bat_infrastructure::{ - lexical_absolute, open_append_file, read_file_no_symlink, read_version_state, + lexical_absolute, open_append_file, read_file_no_symlink, read_version_state, redact_proxy_url, resolve_curl_proxy, validate_output_root, validate_runtime_state_dir, write_file_atomic, CurlProxyConfig, CurlProxyMode, OfficialFailedVersionRecord, OfficialServerInfoSource, OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService, @@ -1668,10 +1668,28 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result) -> Vec { + let mut redacted = command; + for index in 0..redacted.len() { + if redacted[index] == "--proxy" { + if let Some(value) = redacted.get_mut(index + 1) { + *value = redact_proxy_url(value); + } + } + } + redacted +} + #[derive(Debug, Serialize)] struct DaemonControlReport { command: &'static str, @@ -5044,6 +5062,54 @@ mod tests { assert_eq!(OfficialUpdateStatus::Downloaded.as_str(), "downloaded"); } + #[test] + fn redacts_proxy_credentials_in_surfaced_command() { + let command = vec![ + "/opt/bat".to_string(), + "--auto-discover".to_string(), + "--proxy".to_string(), + "http://user:secret@127.0.0.1:7890".to_string(), + "--output".to_string(), + "/var/lib/bat".to_string(), + ]; + + let redacted = redact_command_proxy_credentials(command); + + assert_eq!(redacted[3], "http://@127.0.0.1:7890"); + assert!(!redacted.join(" ").contains("secret")); + // 非代理参数保持原样。 + assert_eq!(redacted[1], "--auto-discover"); + assert_eq!(redacted[5], "/var/lib/bat"); + } + + #[test] + fn redacts_schemeless_proxy_credentials_in_surfaced_command() { + let command = vec![ + "/opt/bat".to_string(), + "--proxy".to_string(), + "user:secret@127.0.0.1:7890".to_string(), + ]; + + let redacted = redact_command_proxy_credentials(command); + + assert_eq!(redacted[2], "@127.0.0.1:7890"); + assert!(!redacted.join(" ").contains("secret")); + } + + #[test] + fn leaves_command_without_proxy_untouched() { + let command = vec![ + "/opt/bat".to_string(), + "--auto-discover".to_string(), + "--proxy".to_string(), + "auto".to_string(), + ]; + + let redacted = redact_command_proxy_credentials(command.clone()); + + assert_eq!(redacted, command); + } + fn beijing_time(day: u64, hour: u64, minute: u64, second: u64) -> SystemTime { system_time_from_beijing_local_seconds( day.saturating_mul(SECONDS_PER_DAY) diff --git a/infrastructure/src/curl_transfer.rs b/infrastructure/src/curl_transfer.rs index fa133c7..c462737 100644 --- a/infrastructure/src/curl_transfer.rs +++ b/infrastructure/src/curl_transfer.rs @@ -108,14 +108,21 @@ pub fn resolve_curl_proxy(config: &CurlProxyConfig) -> ResolvedCurlProxy { } /// Redacts user info from a proxy URL for diagnostics and logs. +/// +/// `curl` accepts proxy strings without an explicit scheme (defaulting to +/// `http://`), so credentials such as `user:secret@host:port` must be redacted +/// even when no `://` separator is present. pub fn redact_proxy_url(url: &str) -> String { - let Some((scheme, rest)) = url.split_once("://") else { + if let Some((scheme, rest)) = url.split_once("://") { + let Some(at_index) = rest.find('@') else { + return url.to_string(); + }; + return format!("{scheme}://@{}", &rest[at_index + 1..]); + } + let Some(at_index) = url.find('@') else { return url.to_string(); }; - let Some(at_index) = rest.find('@') else { - return url.to_string(); - }; - format!("{scheme}://@{}", &rest[at_index + 1..]) + format!("@{}", &url[at_index + 1..]) } fn proxy_env_pairs() -> Vec<(String, String)> { @@ -576,4 +583,21 @@ mod tests { "http://@127.0.0.1:7890" ); } + + #[test] + fn redact_proxy_url_handles_schemeless_credentials() { + assert_eq!( + redact_proxy_url("user:secret@127.0.0.1:7890"), + "@127.0.0.1:7890" + ); + assert_eq!( + redact_proxy_url("socks5://user:secret@127.0.0.1:1080"), + "socks5://@127.0.0.1:1080" + ); + assert_eq!(redact_proxy_url("127.0.0.1:7890"), "127.0.0.1:7890"); + assert_eq!( + redact_proxy_url("http://127.0.0.1:7890"), + "http://127.0.0.1:7890" + ); + } }