mirror of
https://github.com/Yuyi-Oak/BlueArchiveToolkit.git
synced 2026-07-22 04:15:14 +08:00
fix(official-sync): 脱敏代理凭据避免泄漏到日志与状态输出
两处代理凭据泄漏: - 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/<pid>/cmdline 读到代理凭据,以及 0600 状态 文件内的 at-rest 凭据,二者受进程属主与文件权限约束,后续可改由环境变量下 传子进程进一步收敛。 对应 issue #18 维护清单 1-2。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
use bat_adapters::official::yostar_jp::PatchPlatform;
|
use bat_adapters::official::yostar_jp::PatchPlatform;
|
||||||
use bat_infrastructure::{
|
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,
|
resolve_curl_proxy, validate_output_root, validate_runtime_state_dir, write_file_atomic,
|
||||||
CurlProxyConfig, CurlProxyMode, OfficialFailedVersionRecord, OfficialServerInfoSource,
|
CurlProxyConfig, CurlProxyMode, OfficialFailedVersionRecord, OfficialServerInfoSource,
|
||||||
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
|
OfficialUpdateConfig, OfficialUpdateProgress, OfficialUpdateReport, OfficialUpdateService,
|
||||||
@@ -1668,10 +1668,28 @@ fn build_daemon_status_report(state_dir: &Path) -> anyhow::Result<DaemonStatusRe
|
|||||||
next_forced_refresh_unix_seconds: status_file
|
next_forced_refresh_unix_seconds: status_file
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|status| status.next_forced_refresh_unix_seconds),
|
.and_then(|status| status.next_forced_refresh_unix_seconds),
|
||||||
command: status_file.map(|status| status.command),
|
command: status_file.map(|status| redact_command_proxy_credentials(status.command)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Redacts proxy credentials in a saved daemon command before it is surfaced to
|
||||||
|
/// callers (RPC `status`, `bat status --json`, human output).
|
||||||
|
///
|
||||||
|
/// The on-disk status file keeps the raw command because `restart`/`reload`
|
||||||
|
/// relaunch the daemon from it; only the externally visible copy is redacted so
|
||||||
|
/// credentials never reach stdout, monitoring, or log collection.
|
||||||
|
fn redact_command_proxy_credentials(command: Vec<String>) -> Vec<String> {
|
||||||
|
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)]
|
#[derive(Debug, Serialize)]
|
||||||
struct DaemonControlReport {
|
struct DaemonControlReport {
|
||||||
command: &'static str,
|
command: &'static str,
|
||||||
@@ -5044,6 +5062,54 @@ mod tests {
|
|||||||
assert_eq!(OfficialUpdateStatus::Downloaded.as_str(), "downloaded");
|
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://<redacted>@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], "<redacted>@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 {
|
fn beijing_time(day: u64, hour: u64, minute: u64, second: u64) -> SystemTime {
|
||||||
system_time_from_beijing_local_seconds(
|
system_time_from_beijing_local_seconds(
|
||||||
day.saturating_mul(SECONDS_PER_DAY)
|
day.saturating_mul(SECONDS_PER_DAY)
|
||||||
|
|||||||
@@ -108,14 +108,21 @@ pub fn resolve_curl_proxy(config: &CurlProxyConfig) -> ResolvedCurlProxy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Redacts user info from a proxy URL for diagnostics and logs.
|
/// 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 {
|
pub fn redact_proxy_url(url: &str) -> String {
|
||||||
let Some((scheme, rest)) = url.split_once("://") else {
|
if let Some((scheme, rest)) = url.split_once("://") {
|
||||||
return url.to_string();
|
|
||||||
};
|
|
||||||
let Some(at_index) = rest.find('@') else {
|
let Some(at_index) = rest.find('@') else {
|
||||||
return url.to_string();
|
return url.to_string();
|
||||||
};
|
};
|
||||||
format!("{scheme}://<redacted>@{}", &rest[at_index + 1..])
|
return format!("{scheme}://<redacted>@{}", &rest[at_index + 1..]);
|
||||||
|
}
|
||||||
|
let Some(at_index) = url.find('@') else {
|
||||||
|
return url.to_string();
|
||||||
|
};
|
||||||
|
format!("<redacted>@{}", &url[at_index + 1..])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn proxy_env_pairs() -> Vec<(String, String)> {
|
fn proxy_env_pairs() -> Vec<(String, String)> {
|
||||||
@@ -576,4 +583,21 @@ mod tests {
|
|||||||
"http://<redacted>@127.0.0.1:7890"
|
"http://<redacted>@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"),
|
||||||
|
"<redacted>@127.0.0.1:7890"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
redact_proxy_url("socks5://user:secret@127.0.0.1:1080"),
|
||||||
|
"socks5://<redacted>@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"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user