mirror of https://github.com/jackwener/wx-cli.git
Root cause: `wx init` does two conceptually-separate things in one
privileged process: (1) scan WeChat memory for keys (needs root) and
(2) write ~/.wx-cli/{all_keys,config}.json (needs only user). When
run under sudo, the files inherit root ownership, so later the daemon
(forked as the user) can't create daemon.sock/log/pid → silent 15s
timeout.
Also: all_keys.json is the raw AES key; 0644 leaked it to every user
on the system.
Fix in init.rs: after the scan completes, immediately setgid+setuid
back to \$SUDO_UID/\$SUDO_GID and set umask 0o077 before any file I/O.
Files are then created as the real user with 0600 by default. Migrate
old broken installs by chown+chmod-recursive before the setuid call.
Fix in transport.rs: pre-check that ~/.wx-cli/ is writable before
spawning daemon; on EACCES print a clear "sudo chown -R ..." hint
instead of the useless "daemon 启动超时" message.
pull/13/head
parent
ae74072b3f
commit
e44990ba01
|
|
@ -43,6 +43,13 @@ pub fn cmd_init(force: bool) -> Result<()> {
|
|||
println!("扫描加密密钥(需要 root 权限)...");
|
||||
let entries = scanner::scan_keys(&db_dir)?;
|
||||
|
||||
// === 权限边界 ===
|
||||
// 扫描完成后立即 drop 到调用用户身份,后续文件写入都是用户属主。
|
||||
// 未来 daemon(由 `wx sessions` 以用户身份 fork)才能往 ~/.wx-cli/
|
||||
// 写 socket/log/pid。
|
||||
#[cfg(unix)]
|
||||
drop_privileges_if_sudo()?;
|
||||
|
||||
// 确保父目录存在(如 ~/.wx-cli/),必须在任何写入之前
|
||||
if let Some(parent) = config_path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
|
|
@ -89,6 +96,88 @@ pub fn cmd_init(force: bool) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// 如果当前以 root 身份运行且是通过 sudo 启动的,drop 到调用用户身份,
|
||||
/// 并迁移旧版本遗留的 root 属主 `~/.wx-cli/`。
|
||||
///
|
||||
/// 只影响本进程;daemon(后续 fork)会继承调用用户身份。
|
||||
#[cfg(unix)]
|
||||
fn drop_privileges_if_sudo() -> Result<()> {
|
||||
use std::ffi::CString;
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
use std::path::Path;
|
||||
|
||||
// 当前不是 root(用户直接以非 root 跑的 `wx init`)→ 什么都不做
|
||||
if unsafe { libc::geteuid() } != 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let sudo_uid: Option<u32> = std::env::var("SUDO_UID").ok().and_then(|s| s.parse().ok());
|
||||
let sudo_gid: Option<u32> = std::env::var("SUDO_GID").ok().and_then(|s| s.parse().ok());
|
||||
let (uid, gid) = match (sudo_uid, sudo_gid) {
|
||||
(Some(u), Some(g)) if u != 0 => (u, g),
|
||||
// 直接以 root 登陆(非 sudo),没有"调用用户"可还原 → 保持 root
|
||||
_ => return Ok(()),
|
||||
};
|
||||
|
||||
// 迁移旧版本遗留:如果 ~/.wx-cli/ 已存在且属 root,把它 chown 回调用用户,
|
||||
// 顺便把 raw key 文件的权限也收紧到 0600(旧版默认 0644,世界可读等于泄露)。
|
||||
// 这些必须在 setuid 之前做:chown 需要 root,chmod 也只有属主或 root 能改。
|
||||
let cli_dir = config::cli_dir();
|
||||
if cli_dir.exists() {
|
||||
let _ = chown_recursive(&cli_dir, uid, gid);
|
||||
let _ = tighten_perms(&cli_dir);
|
||||
}
|
||||
|
||||
// 设置 umask,让后续 create 出来的文件/目录默认是 0600 / 0700。
|
||||
unsafe { libc::umask(0o077); }
|
||||
|
||||
// 必须先 setgid 再 setuid:一旦 uid 降下来就没法再改 gid 了。
|
||||
unsafe {
|
||||
if libc::setgid(gid) != 0 {
|
||||
anyhow::bail!("setgid({}) 失败: {}", gid, std::io::Error::last_os_error());
|
||||
}
|
||||
if libc::setuid(uid) != 0 {
|
||||
anyhow::bail!("setuid({}) 失败: {}", uid, std::io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
|
||||
// chown 递归实现
|
||||
fn chown_recursive(path: &Path, uid: u32, gid: u32) -> std::io::Result<()> {
|
||||
chown_one(path, uid, gid)?;
|
||||
let md = std::fs::symlink_metadata(path)?;
|
||||
if md.is_dir() {
|
||||
for entry in std::fs::read_dir(path)? {
|
||||
chown_recursive(&entry?.path(), uid, gid)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn chown_one(path: &Path, uid: u32, gid: u32) -> std::io::Result<()> {
|
||||
let c = CString::new(path.as_os_str().as_bytes())
|
||||
.map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "path contains NUL"))?;
|
||||
if unsafe { libc::chown(c.as_ptr(), uid, gid) } != 0 {
|
||||
return Err(std::io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 目录收紧到 0700,所有 *.json 文件(含 all_keys.json 这类 raw key)收紧到 0600。
|
||||
fn tighten_perms(cli_dir: &Path) -> std::io::Result<()> {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(cli_dir, std::fs::Permissions::from_mode(0o700))?;
|
||||
for entry in std::fs::read_dir(cli_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
if path.extension().and_then(|s| s.to_str()) == Some("json") {
|
||||
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn find_or_create_config_path() -> std::path::PathBuf {
|
||||
// 如果当前工作目录或可执行文件目录已有 config.json,沿用它(支持便携模式)
|
||||
if let Ok(cwd) = std::env::current_dir() {
|
||||
|
|
|
|||
|
|
@ -62,10 +62,47 @@ pub fn ensure_daemon() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// 启动 daemon 前检查 `~/.wx-cli/` 可写,给出比"超时"更明确的错误。
|
||||
///
|
||||
/// 典型坑:旧版本 `sudo wx init` 把目录留成 root 属主,非 root 的 daemon
|
||||
/// 连 socket/log 都建不了,会静默失败 15s 超时。
|
||||
fn preflight_cli_dir_writable() -> Result<()> {
|
||||
let cli_dir = config::cli_dir();
|
||||
std::fs::create_dir_all(&cli_dir)
|
||||
.with_context(|| format!("创建 {} 失败", cli_dir.display()))?;
|
||||
|
||||
let probe = cli_dir.join(".daemon_probe");
|
||||
match std::fs::File::create(&probe) {
|
||||
Ok(_) => {
|
||||
let _ = std::fs::remove_file(&probe);
|
||||
Ok(())
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => {
|
||||
let dir = cli_dir.display();
|
||||
if cfg!(unix) {
|
||||
bail!(
|
||||
"无法写入 {dir}(权限不足)\n\n\
|
||||
这通常是老版本的 `sudo wx init` 把目录属主留成了 root。\n\
|
||||
修复:\n\n \
|
||||
sudo chown -R $(whoami) {dir}\n\n\
|
||||
(新版已修复此问题,下次 init 不会再发生)",
|
||||
)
|
||||
} else {
|
||||
bail!("无法写入 {dir}: {e}")
|
||||
}
|
||||
}
|
||||
Err(e) => bail!("无法写入 {}: {}", cli_dir.display(), e),
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动 daemon 进程(自身二进制,设置 WX_DAEMON_MODE=1)
|
||||
fn start_daemon() -> Result<()> {
|
||||
let exe = std::env::current_exe().context("无法获取当前可执行文件路径")?;
|
||||
|
||||
// 预检:当前用户是否能写 ~/.wx-cli/。如果不能,给出可操作的错误信息,
|
||||
// 而不是 spawn 一个注定失败的 daemon 然后超时 15s。
|
||||
preflight_cli_dir_writable()?;
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
|
|
|
|||
Loading…
Reference in New Issue