diff --git a/Cargo.toml b/Cargo.toml index 7e74c7d..6f60830 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" } diff --git a/src/config.rs b/src/config.rs index e99a90c..41e4061 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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")] diff --git a/src/lib.rs b/src/lib.rs index 675a833..bf7bbf3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/platform/linux.rs b/src/platform/linux.rs index 7ebb9bc..d4b29bb 100644 --- a/src/platform/linux.rs +++ b/src/platform/linux.rs @@ -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> { 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 { + 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'"); + } }