From 565fd317de6eea3711499264d6ad9b5413638d38 Mon Sep 17 00:00:00 2001 From: Yuyi-Oak <1722157266@qq.com> Date: Thu, 16 Jul 2026 06:44:03 -0700 Subject: [PATCH] =?UTF-8?q?fix(official-sync):=20curl=20=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E6=94=B9=E7=94=B1=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F=E4=BC=A0?= =?UTF-8?q?=E5=85=A5=EF=BC=8C=E5=A0=B5=E4=BD=8F=20argv=20=E6=B3=84?= =?UTF-8?q?=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit apply_curl_proxy 原来对 URL 型代理执行 command.arg("--proxy").arg(url), 把可能含凭据的代理 URL 放进 curl 子进程 argv,会出现在 curl 进程的 /proc//cmdline(0444,世界可读,每次下载存活期间可被本地任意用户读到)。 改为先 remove_proxy_env 清除所有继承的代理变量,再通过 ALL_PROXY/all_proxy 环境变量把代理传给 curl(大小写两种写法都设,避免 curl 只读其一)。凭据从 世界可读的 cmdline 转移到仅属主可读的 /proc//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 --- infrastructure/src/curl_transfer.rs | 75 ++++++++++++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/infrastructure/src/curl_transfer.rs b/infrastructure/src/curl_transfer.rs index c462737..ab2920b 100644 --- a/infrastructure/src/curl_transfer.rs +++ b/infrastructure/src/curl_transfer.rs @@ -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//cmdline(0444,世界可读)。 + // 先清除所有继承的代理变量,再设 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 { + command + .get_args() + .map(|arg| arg.to_string_lossy().to_string()) + .collect() + } + + fn command_env(command: &Command, key: &str) -> Option> { + 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)); + } }