安全: 令牌文件未设置限制性权限 (cap_portal.rs) #2

Closed
opened 2026-06-04 20:58:16 +08:00 by dailz · 1 comment
Owner

位置

src/cap_portal.rs:262-271

严重性

🟡

问题描述

std::fs::write 创建文件时使用默认 umask 权限(通常为 0644),导致令牌文件可能被其他用户读取。同时 create_dir_all 创建的目录也未设置限制权限。

如果在多用户 Linux 系统上,攻击者读取该令牌后可能绕过用户授权直接恢复屏幕录制会话,构成权限绕过和信息泄露。

建议修复

使用 OpenOptions 配合 Unix 的 PermissionsExt::mode(0o600) 设置仅当前用户可读写。目录也应设置为 0o700。建议使用原子写入(tmp file + rename)模式:

fn save_restore_token(token: &str) {
    use std::fs::OpenOptions;
    use std::io::Write;
    use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};

    let path = token_path();
    if let Some(parent) = path.parent() {
        if let Err(e) = std::fs::create_dir_all(parent) {
            tracing::warn!("Failed to create token dir: {e}");
            return;
        }
        let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700));
    }

    let tmp_path = path.with_extension("tmp");
    let result = (|| -> std::io::Result<()> {
        let mut f = OpenOptions::new()
            .write(true)
            .create(true)
            .truncate(true)
            .mode(0o600)
            .open(&tmp_path)?;
        f.write_all(token.as_bytes())?;
        f.sync_all()?;
        std::fs::rename(&tmp_path, &path)?;
        Ok(())
    })();
    match result {
        Ok(()) => tracing::info!("Saved portal restore token"),
        Err(e) => {
            let _ = std::fs::remove_file(&tmp_path);
            tracing::warn!("Failed to save restore token: {e}");
        }
    }
}
## 位置 `src/cap_portal.rs:262-271` ## 严重性 🟡 中 ## 问题描述 `std::fs::write` 创建文件时使用默认 umask 权限(通常为 0644),导致令牌文件可能被其他用户读取。同时 `create_dir_all` 创建的目录也未设置限制权限。 如果在多用户 Linux 系统上,攻击者读取该令牌后可能绕过用户授权直接恢复屏幕录制会话,构成权限绕过和信息泄露。 ## 建议修复 使用 `OpenOptions` 配合 Unix 的 `PermissionsExt::mode(0o600)` 设置仅当前用户可读写。目录也应设置为 0o700。建议使用原子写入(tmp file + rename)模式: ```rust fn save_restore_token(token: &str) { use std::fs::OpenOptions; use std::io::Write; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; let path = token_path(); if let Some(parent) = path.parent() { if let Err(e) = std::fs::create_dir_all(parent) { tracing::warn!("Failed to create token dir: {e}"); return; } let _ = std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700)); } let tmp_path = path.with_extension("tmp"); let result = (|| -> std::io::Result<()> { let mut f = OpenOptions::new() .write(true) .create(true) .truncate(true) .mode(0o600) .open(&tmp_path)?; f.write_all(token.as_bytes())?; f.sync_all()?; std::fs::rename(&tmp_path, &path)?; Ok(()) })(); match result { Ok(()) => tracing::info!("Saved portal restore token"), Err(e) => { let _ = std::fs::remove_file(&tmp_path); tracing::warn!("Failed to save restore token: {e}"); } } } ```
dailz closed this issue 2026-06-06 11:05:07 +08:00
Author
Owner

修复完成

已在 commit 9a5b09c 中修复,基于 Oracle 安全审计建议实施。

改动摘要

token_path() — 返回 Option<PathBuf>,移除 /tmp fallback

save_restore_token()

  • create_new(true).mode(0o600) 独占创建,防止符号链接攻击
  • {pid}.tmp 不可预测临时文件名
  • DirBuilderExt::mode(0o700) 创建目录绕过 umask
  • 目录元数据验证(属主/权限/非符号链接)

load_restore_token()

  • 拒绝符号链接、非普通文件、非当前用户属主、group/other 可读文件

新增测试(11 个)

场景 测试
不 fallback 到 /tmp token_path_never_uses_tmp
目录权限验证 verify_secure_dir_rejects_wrong_permissions
非目录路径拒绝 verify_secure_dir_rejects_non_directory
新建目录 0o700 ensure_secure_parent_creates_with_0700
收紧已有目录权限 ensure_secure_parent_tightens_existing_dir
文件权限 0o600 save_creates_file_with_0600
正常读取安全文件 load_reads_secure_file
拒绝 group 可读 load_rejects_group_readable_file
拒绝 world 可读 load_rejects_world_readable_file
拒绝符号链接 load_rejects_symlink
写入-读取往返 save_then_load_roundtrip
## 修复完成 已在 commit [9a5b09c](https://ni.dailz.cn:30002/dailz/wl-webrtc/commit/9a5b09c) 中修复,基于 Oracle 安全审计建议实施。 ### 改动摘要 **`token_path()`** — 返回 `Option<PathBuf>`,移除 `/tmp` fallback **`save_restore_token()`** - `create_new(true).mode(0o600)` 独占创建,防止符号链接攻击 - `{pid}.tmp` 不可预测临时文件名 - `DirBuilderExt::mode(0o700)` 创建目录绕过 umask - 目录元数据验证(属主/权限/非符号链接) **`load_restore_token()`** - 拒绝符号链接、非普通文件、非当前用户属主、group/other 可读文件 ### 新增测试(11 个) | 场景 | 测试 | |---|---| | 不 fallback 到 /tmp | `token_path_never_uses_tmp` | | 目录权限验证 | `verify_secure_dir_rejects_wrong_permissions` | | 非目录路径拒绝 | `verify_secure_dir_rejects_non_directory` | | 新建目录 0o700 | `ensure_secure_parent_creates_with_0700` | | 收紧已有目录权限 | `ensure_secure_parent_tightens_existing_dir` | | 文件权限 0o600 | `save_creates_file_with_0600` | | 正常读取安全文件 | `load_reads_secure_file` | | 拒绝 group 可读 | `load_rejects_group_readable_file` | | 拒绝 world 可读 | `load_rejects_world_readable_file` | | 拒绝符号链接 | `load_rejects_symlink` | | 写入-读取往返 | `save_then_load_roundtrip` |
Sign in to join this conversation.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dailz/wl-webrtc#2