cross_path/
security.rs

1use crate::{PathError, PathResult};
2use regex::Regex;
3use std::path::Path;
4
5/// Path security checker for preventing path-based attacks
6#[derive(Debug, Clone)]
7pub struct PathSecurityChecker {
8    path_traversal_regex: Regex,
9    dangerous_patterns: Vec<Regex>,
10    #[allow(dead_code)] // Reserved names are only used on Windows
11    reserved_names: Vec<&'static str>,
12}
13
14impl Default for PathSecurityChecker {
15    fn default() -> Self {
16        Self {
17            path_traversal_regex: Regex::new(r"(\.\./|\.\.\\)").unwrap(),
18            dangerous_patterns: vec![
19                Regex::new(r"(?i)\.(exe|bat|cmd|sh|php|py|js)$").unwrap(),
20                Regex::new(r"^/proc/").unwrap(),
21                Regex::new(r"^/dev/").unwrap(),
22                Regex::new(r"^/sys/").unwrap(),
23            ],
24            reserved_names: vec![
25                "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7",
26                "COM8", "COM9", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8",
27                "LPT9",
28            ],
29        }
30    }
31}
32
33impl PathSecurityChecker {
34    /// Create new security checker
35    #[must_use]
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// Check path security (static method)
41    ///
42    /// # Errors
43    ///
44    /// Returns `PathError` if the path is considered unsafe.
45    pub fn check_path_security(path: &Path) -> PathResult<bool> {
46        let checker = Self::new();
47        checker.check(path)
48    }
49
50    /// Perform security checks on path
51    ///
52    /// # Errors
53    ///
54    /// Returns `PathError` if the path violates any security rules.
55    pub fn check(&self, path: &Path) -> PathResult<bool> {
56        // Check for path traversal attacks
57        if self.detect_path_traversal(path) {
58            return Err(PathError::security_error("Path traversal attack detected"));
59        }
60
61        // Check for dangerous patterns
62        if self.contains_dangerous_patterns(path) {
63            return Err(PathError::security_error(
64                "Path contains dangerous patterns",
65            ));
66        }
67
68        // Check for reserved names (Windows)
69        if self.contains_reserved_names(path) {
70            return Err(PathError::security_error(
71                "Path contains Windows reserved names",
72            ));
73        }
74
75        // Check for system directory access attempts
76        if Self::accesses_system_directories(path) {
77            return Err(PathError::security_error(
78                "Attempt to access system directories",
79            ));
80        }
81
82        Ok(true)
83    }
84
85    /// Detect path traversal patterns
86    fn detect_path_traversal(&self, path: &Path) -> bool {
87        let path_str = path.to_string_lossy();
88        self.path_traversal_regex.is_match(&path_str)
89    }
90
91    /// Check for dangerous file patterns
92    fn contains_dangerous_patterns(&self, path: &Path) -> bool {
93        let path_str = path.to_string_lossy();
94        self.dangerous_patterns
95            .iter()
96            .any(|re| re.is_match(&path_str))
97    }
98
99    /// Check for Windows reserved names
100    #[allow(clippy::unused_self)]
101    fn contains_reserved_names(&self, path: &Path) -> bool {
102        #[cfg(target_os = "windows")]
103        {
104            if let Some(file_name) = path.file_name() {
105                let name = file_name.to_string_lossy();
106                let name_without_ext = name.split('.').next().unwrap_or("");
107                self.reserved_names
108                    .iter()
109                    .any(|&reserved| name_without_ext.eq_ignore_ascii_case(reserved))
110            } else {
111                false
112            }
113        }
114        #[cfg(not(target_os = "windows"))]
115        {
116            // On non-Windows systems, we generally don't need to check for Windows reserved names
117            // unless we are specifically validating for cross-platform compatibility.
118            // For now, we skip this check to avoid false positives on valid Unix filenames.
119            let _ = path; // Suppress unused variable warning
120            false
121        }
122    }
123
124    /// Check if path attempts to access system directories
125    fn accesses_system_directories(path: &Path) -> bool {
126        let path_str = path.to_string_lossy();
127
128        #[cfg(target_os = "windows")]
129        {
130            let system_dirs = vec![
131                r"C:\Windows",
132                r"C:\System32",
133                r"C:\Program Files",
134                r"C:\ProgramData",
135            ];
136            system_dirs.iter().any(|&dir| path_str.starts_with(dir))
137        }
138
139        #[cfg(not(target_os = "windows"))]
140        {
141            // Common Unix system directories
142            // Covers Linux, macOS, FreeBSD, OpenBSD, Android, etc.
143            let system_dirs = vec![
144                "/bin",
145                "/sbin",
146                "/usr/bin",
147                "/usr/sbin",
148                "/etc",
149                "/root",
150                "/var",
151                "/lib",
152                "/boot",
153                "/dev",
154                "/proc",
155                "/sys",
156            ];
157
158            // Android specific system directories
159            #[cfg(target_os = "android")]
160            let system_dirs = {
161                let mut dirs = system_dirs;
162                dirs.extend_from_slice(&["/system", "/data", "/cache", "/vendor", "/oem", "/odm"]);
163                dirs
164            };
165
166            // macOS specific system directories
167            #[cfg(target_os = "macos")]
168            let system_dirs = {
169                let mut dirs = system_dirs;
170                dirs.extend_from_slice(&[
171                    "/System", "/Library", "/private", "/Volumes", "/Network",
172                ]);
173                dirs
174            };
175
176            system_dirs.iter().any(|&dir| path_str.starts_with(dir))
177        }
178    }
179
180    /// Sanitize path by removing dangerous characters
181    #[must_use]
182    pub fn sanitize_path(path: &str) -> String {
183        let mut sanitized = path.to_string();
184
185        // Remove path traversal sequences
186        sanitized = sanitized.replace("../", "").replace("..\\", "");
187
188        // Remove dangerous characters
189        let dangerous = ['<', '>', ':', '"', '|', '?', '*', '\\', '/', '\0'];
190        for c in dangerous {
191            sanitized = sanitized.replace(c, "_");
192        }
193
194        // Limit path length
195        if sanitized.len() > 255 {
196            sanitized = sanitized[..255].to_string();
197        }
198
199        sanitized
200    }
201}