diff --git a/server_manager/src/core/users.rs b/server_manager/src/core/users.rs index 46ff68a..eea3256 100644 --- a/server_manager/src/core/users.rs +++ b/server_manager/src/core/users.rs @@ -363,7 +363,10 @@ impl UserManager { } pub fn list_users(&self) -> Vec<&User> { - self.users.values().collect() + let mut users: Vec<&User> = Vec::with_capacity(self.users.len()); + users.extend(self.users.values()); + users.sort_by(|a, b| a.username.cmp(&b.username)); + users } } @@ -537,4 +540,22 @@ mod tests { assert!(!Role::Observer.can_trigger_updates()); assert!(!Role::Auditor.can_trigger_updates()); } + + #[test] + fn test_list_users_deterministic_sorting() { + let mut manager = UserManager::default(); + manager + .add_user("charlie", "pass123", Role::Observer, None) + .expect("User charlie creation failed"); + manager + .add_user("alice", "pass123", Role::Admin, None) + .expect("User alice creation failed"); + manager + .add_user("bob", "pass123", Role::Operator, None) + .expect("User bob creation failed"); + + let list = manager.list_users(); + let usernames: Vec<&str> = list.iter().map(|u| u.username.as_str()).collect(); + assert_eq!(usernames, vec!["alice", "bob", "charlie"]); + } } diff --git a/server_manager/src/core/validate.rs b/server_manager/src/core/validate.rs index 9c4898e..17809c8 100644 --- a/server_manager/src/core/validate.rs +++ b/server_manager/src/core/validate.rs @@ -139,10 +139,17 @@ pub fn validate_ip(ip_str: &str) -> Result { .map_err(|_| anyhow::anyhow!("Validation error: invalid IP address '{}'", ip_str)) } -/// Validates that a path does not contain directory traversal sequences (`..`). +/// Validates that a path does not contain directory traversal sequences (`..`), NUL bytes, or ASCII control characters. pub fn validate_safe_path>(path: P) -> Result

{ let p = path.as_ref(); - let normalized = p.to_string_lossy().replace('\\', "/"); + let path_str = p.to_string_lossy(); + if path_str.bytes().any(|b| b == 0 || (b < 32 && b != b'\t')) { + bail!( + "Validation error: forbidden control characters or NUL byte in path '{}'", + p.display() + ); + } + let normalized = path_str.replace('\\', "/"); let norm_path = Path::new(&normalized); for comp in norm_path.components() { if comp == Component::ParentDir { diff --git a/server_manager/tests/contract_input_validation.rs b/server_manager/tests/contract_input_validation.rs index 7b9da2d..2d27553 100644 --- a/server_manager/tests/contract_input_validation.rs +++ b/server_manager/tests/contract_input_validation.rs @@ -121,3 +121,10 @@ fn test_validate_safe_path_extended_traversal() { assert!(validate_safe_path(Path::new("..")).is_err()); assert!(validate_safe_path(Path::new(".")).is_ok()); } + +#[test] +fn test_validate_safe_path_control_chars() { + assert!(validate_safe_path(Path::new("file\0.txt")).is_err()); + assert!(validate_safe_path(Path::new("file\r\n.txt")).is_err()); + assert!(validate_safe_path(Path::new("file\x07.txt")).is_err()); +}