fix(official-sync): curl 代理改由环境变量传入,堵住 argv 泄漏

apply_curl_proxy 原来对 URL 型代理执行 command.arg("--proxy").arg(url),
把可能含凭据的代理 URL 放进 curl 子进程 argv,会出现在 curl 进程的
/proc/<pid>/cmdline(0444,世界可读,每次下载存活期间可被本地任意用户读到)。

改为先 remove_proxy_env 清除所有继承的代理变量,再通过 ALL_PROXY/all_proxy
环境变量把代理传给 curl(大小写两种写法都设,避免 curl 只读其一)。凭据从
世界可读的 cmdline 转移到仅属主可读的 /proc/<pid>/environ(0400)。no_proxy
列表不含凭据,仍以 --noproxy 参数传入;disabled 模式保持 --noproxy * 直连。

至此代理凭据泄漏面全部堵住:daemon 子进程 argv、0600 状态文件(55b9bab),
以及本次的 curl 孙进程 argv。

验证:
- 新增单测用 Command::get_args/get_envs 断言 URL 只进 ALL_PROXY 环境变量、
  不进 argv,且 scheme 专用变量被清除;disabled 模式强制直连。
- 真机经本地混合代理 127.0.0.1:7897 端到端验证:curl shim 记录 11 次调用,
  凭据 0 次进 argv、0 个 --proxy 参数,代理仅在 ALL_PROXY 环境变量中;curl
  确实经该代理到达官方 launcher/CDN/resources.assets 端点,功能未回归。
- 对抗性审查确认 auto/disabled/no_proxy 语义与旧 --proxy 等价,env 先删后设
  最终为 set,无其它遗漏的凭据落 argv/日志/状态文件路径。

对应 issue #18 维护清单 1-2 残留项(curl 子进程)。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 06:44:03 -07:00
co-authored by Claude Fable 5
parent 55b9babb66
commit 565fd317de
+74 -1
View File
@@ -423,8 +423,13 @@ fn apply_curl_proxy(command: &mut Command, proxy: &ResolvedCurlProxy) {
}
if let Some(url) = proxy.url.as_ref() {
// 通过环境变量而非 --proxy 参数把代理传给 curl:代理 URL 可能带凭据,
// 放进 argv 会出现在 curl 子进程的 /proc/<pid>/cmdline0444,世界可读)。
// 先清除所有继承的代理变量,再设 ALL_PROXY/all_proxy(覆盖 http 与 https
// 大小写两种写法都设,避免 curl 只读其中一种)。no_proxy 列表不含凭据,仍走参数。
remove_proxy_env(command);
command.arg("--proxy").arg(url);
command.env("ALL_PROXY", url);
command.env("all_proxy", url);
if let Some(no_proxy) = proxy.no_proxy.as_ref() {
command.arg("--noproxy").arg(no_proxy);
}
@@ -600,4 +605,72 @@ mod tests {
"http://127.0.0.1:7890"
);
}
fn command_args(command: &Command) -> Vec<String> {
command
.get_args()
.map(|arg| arg.to_string_lossy().to_string())
.collect()
}
fn command_env(command: &Command, key: &str) -> Option<Option<String>> {
command
.get_envs()
.find(|(name, _)| *name == key)
.map(|(_, value)| value.map(|value| value.to_string_lossy().to_string()))
}
#[test]
fn apply_curl_proxy_passes_url_via_env_not_argv() {
let proxy = ResolvedCurlProxy {
enabled: true,
disabled: false,
url: Some("http://user:secret@127.0.0.1:7897".to_string()),
source: "--proxy".to_string(),
no_proxy: Some("localhost,127.0.0.1".to_string()),
};
let mut command = Command::new("curl");
apply_curl_proxy(&mut command, &proxy);
// 凭据不得出现在 curl 的 argv。
let args = command_args(&command);
assert!(!args.iter().any(|arg| arg.contains("secret")));
assert!(!args.iter().any(|arg| arg == "--proxy"));
// no_proxy 列表不含凭据,仍以参数传入。
assert!(args
.windows(2)
.any(|pair| pair == ["--noproxy", "localhost,127.0.0.1"]));
// 代理 URL 经 ALL_PROXY/all_proxy 环境变量下传。
assert_eq!(
command_env(&command, "ALL_PROXY"),
Some(Some("http://user:secret@127.0.0.1:7897".to_string()))
);
assert_eq!(
command_env(&command, "all_proxy"),
Some(Some("http://user:secret@127.0.0.1:7897".to_string()))
);
// 继承的 scheme 专用代理变量被清除,避免覆盖 ALL_PROXY。
assert_eq!(command_env(&command, "HTTPS_PROXY"), Some(None));
assert_eq!(command_env(&command, "https_proxy"), Some(None));
}
#[test]
fn apply_curl_proxy_disabled_forces_direct() {
let proxy = ResolvedCurlProxy {
enabled: false,
disabled: true,
url: None,
source: "--no-proxy".to_string(),
no_proxy: None,
};
let mut command = Command::new("curl");
apply_curl_proxy(&mut command, &proxy);
let args = command_args(&command);
assert!(args.windows(2).any(|pair| pair == ["--noproxy", "*"]));
// 直连模式清除所有继承的代理变量,且不设置任何代理。
assert_eq!(command_env(&command, "ALL_PROXY"), Some(None));
assert_eq!(command_env(&command, "HTTPS_PROXY"), Some(None));
}
}