1use crate::{PathError, PathResult};
2use regex::Regex;
3use std::path::Path;
4
5#[derive(Debug, Clone)]
7pub struct PathSecurityChecker {
8 path_traversal_regex: Regex,
9 dangerous_patterns: Vec<Regex>,
10 #[allow(dead_code)] 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 #[must_use]
36 pub fn new() -> Self {
37 Self::default()
38 }
39
40 pub fn check_path_security(path: &Path) -> PathResult<bool> {
46 let checker = Self::new();
47 checker.check(path)
48 }
49
50 pub fn check(&self, path: &Path) -> PathResult<bool> {
56 if self.detect_path_traversal(path) {
58 return Err(PathError::security_error("Path traversal attack detected"));
59 }
60
61 if self.contains_dangerous_patterns(path) {
63 return Err(PathError::security_error(
64 "Path contains dangerous patterns",
65 ));
66 }
67
68 if self.contains_reserved_names(path) {
70 return Err(PathError::security_error(
71 "Path contains Windows reserved names",
72 ));
73 }
74
75 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 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 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 #[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 let _ = path; false
121 }
122 }
123
124 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 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 #[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 #[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 #[must_use]
182 pub fn sanitize_path(path: &str) -> String {
183 let mut sanitized = path.to_string();
184
185 sanitized = sanitized.replace("../", "").replace("..\\", "");
187
188 let dangerous = ['<', '>', ':', '"', '|', '?', '*', '\\', '/', '\0'];
190 for c in dangerous {
191 sanitized = sanitized.replace(c, "_");
192 }
193
194 if sanitized.len() > 255 {
196 sanitized = sanitized[..255].to_string();
197 }
198
199 sanitized
200 }
201}