feat: linux, get_home_trusted

Signed-off-by: fufesou <linlong1266@gmail.com>
This commit is contained in:
fufesou
2025-12-21 23:41:27 +08:00
parent 8b0e258673
commit 6df5c2c0ce
4 changed files with 141 additions and 1 deletions

View File

@@ -95,3 +95,4 @@ osascript = "0.3"
sctk = { package = "smithay-client-toolkit", version = "0.20.0", default-features = false, features = [
"calloop",
] }
users = { version = "0.11" }

View File

@@ -626,6 +626,23 @@ impl Config {
(self.id.is_empty() && self.enc_id.is_empty()) || self.key_pair.0.is_empty()
}
/// Get the user's home directory for configuration purposes.
///
/// # Security Note
/// This function uses `dirs_next::home_dir()` which reads the `$HOME` environment
/// variable on Unix systems. This is acceptable for user-space operations (config
/// file storage, logging) where the user may intentionally redirect their home
/// directory.
///
/// **DO NOT use this function in privileged contexts** (e.g., code executed via
/// `gtk_sudo` or system services running as root). For privileged operations on
/// Linux, use `crate::platform::linux::get_home_dir_trusted()` which bypasses
/// the `$HOME` environment variable and queries the system password database
/// directly via `getpwuid`.
///
/// Using `$HOME` in privileged contexts creates a confused-deputy vulnerability
/// where an attacker can manipulate the environment variable to inject malicious
/// paths into privileged operations.
pub fn get_home() -> PathBuf {
#[cfg(any(target_os = "android", target_os = "ios"))]
return PathBuf::from(APP_HOME_DIR.read().unwrap().as_str());
@@ -666,6 +683,12 @@ impl Config {
}
}
/// Get the log directory path.
///
/// # Security Note
/// On macOS, this function uses `dirs_next::home_dir()` which reads the `$HOME`
/// environment variable. On Linux/Android, it uses `Self::get_home()`.
/// See [`Self::get_home()`] for security considerations regarding `$HOME` usage.
#[allow(unreachable_code)]
pub fn log_path() -> PathBuf {
#[cfg(target_os = "macos")]

View File

@@ -68,6 +68,8 @@ pub use whoami;
pub mod tls;
pub mod verifier;
pub use async_recursion;
#[cfg(target_os = "linux")]
pub use users;
pub type SessionID = uuid::Uuid;

View File

@@ -1,5 +1,10 @@
use crate::ResultType;
use std::{collections::HashMap, process::Command};
use std::{
collections::HashMap,
path::{Path, PathBuf},
process::Command,
};
use users::{get_current_uid, get_user_by_uid, os::unix::UserExt};
use sctk::{
output::OutputData,
@@ -442,6 +447,56 @@ pub fn get_wayland_displays() -> ResultType<Vec<WaylandDisplayInfo>> {
Ok(display_infos)
}
/// Escape a string for safe use in shell commands by wrapping in single quotes.
///
/// This function handles the edge case of single quotes within the string by:
/// 1. Ending the current single-quoted section
/// 2. Adding an escaped single quote
/// 3. Starting a new single-quoted section
///
/// Example: "it's here" -> "'it'\''s here'"
#[inline]
pub fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace("'", "'\\''"))
}
/// Get the current user's home directory via getpwuid (trusted source).
///
/// This function uses the system's password database (via `getpwuid`) to retrieve
/// the home directory, avoiding the security risk of relying on the `HOME`
/// environment variable which can be manipulated by untrusted input.
///
/// # Returns
/// - `Some(PathBuf)` if the home directory was found and exists
/// - `None` if the user lookup failed or the directory doesn't exist
///
/// # Security
/// This function is designed to be safe against confused-deputy attacks where
/// an attacker might manipulate environment variables to influence privileged
/// operations.
pub fn get_home_dir_trusted() -> Option<PathBuf> {
let uid = get_current_uid();
match get_user_by_uid(uid) {
Some(user) => {
let home = user.home_dir();
if Path::is_dir(home) {
Some(PathBuf::from(home))
} else {
log::warn!(
"Home directory for uid {} does not exist or is not a directory: {:?}",
uid,
home
);
None
}
}
None => {
log::warn!("Failed to get user info for uid {}", uid);
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -455,4 +510,63 @@ mod tests {
run_cmds("whoami").unwrap()
);
}
/// Test get_home_dir_trusted: returns valid path and ignores HOME env var
#[test]
fn test_get_home_dir_trusted() {
let original_home = std::env::var("HOME").ok();
// Set HOME to a fake/malicious path
std::env::set_var("HOME", "/tmp/fake_malicious_home");
let result = get_home_dir_trusted();
// Restore original HOME
match original_home {
Some(home) => std::env::set_var("HOME", home),
None => std::env::remove_var("HOME"),
}
// Verify: returns valid path that is NOT the fake HOME
if let Some(path) = result {
assert!(path.is_absolute(), "Path should be absolute: {:?}", path);
assert!(path.is_dir(), "Path should be a directory: {:?}", path);
assert_ne!(
path.to_string_lossy(),
"/tmp/fake_malicious_home",
"Should not use HOME env var"
);
}
}
/// Test shell_quote with normal strings
#[test]
fn test_shell_quote_normal() {
assert_eq!(shell_quote("hello"), "'hello'");
assert_eq!(shell_quote("/home/user"), "'/home/user'");
}
/// Test shell_quote with spaces
#[test]
fn test_shell_quote_spaces() {
assert_eq!(shell_quote("/home/my user/file"), "'/home/my user/file'");
assert_eq!(shell_quote("path with spaces"), "'path with spaces'");
}
/// Test shell_quote with single quotes (the tricky case)
#[test]
fn test_shell_quote_single_quotes() {
assert_eq!(shell_quote("it's"), "'it'\\''s'");
assert_eq!(shell_quote("don't stop"), "'don'\\''t stop'");
}
/// Test shell_quote with shell metacharacters
#[test]
fn test_shell_quote_metacharacters() {
// These should all be safely quoted
assert_eq!(shell_quote("test;rm -rf /"), "'test;rm -rf /'");
assert_eq!(shell_quote("$(whoami)"), "'$(whoami)'");
assert_eq!(shell_quote("`id`"), "'`id`'");
assert_eq!(shell_quote("a && b"), "'a && b'");
assert_eq!(shell_quote("a | b"), "'a | b'");
}
}