rust_serv/path_security/
validator.rs1use crate::error::{Error, Result};
2use std::path::{Path, PathBuf};
3
4#[derive(Clone)]
6pub struct PathValidator {
7 root: PathBuf,
8}
9
10impl PathValidator {
11 pub fn new(root: PathBuf) -> Self {
13 Self { root }
14 }
15
16 pub fn validate(&self, path: &Path) -> Result<PathBuf> {
18 let canonical_root = std::fs::canonicalize(&self.root)
20 .map_err(|e| Error::PathSecurity(format!("Failed to canonicalize root: {}", e)))?;
21
22 let canonical_path = match std::fs::canonicalize(path) {
25 Ok(p) => p,
26 Err(e) => {
27 if e.kind() == std::io::ErrorKind::NotFound {
29 let normalized = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
31 return Ok(normalized);
32 }
33 return Err(Error::PathSecurity(format!("Failed to canonicalize path: {}", e)));
34 }
35 };
36
37 if !canonical_path.starts_with(&canonical_root) {
39 return Err(Error::Forbidden("Path outside root directory".to_string()));
40 }
41
42 Ok(canonical_path)
43 }
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use tempfile::TempDir;
50
51 #[test]
52 fn test_validate_valid_path() {
53 let temp_dir = TempDir::new().unwrap();
54 let validator = PathValidator::new(temp_dir.path().to_path_buf());
55
56 let test_file = temp_dir.path().join("test.txt");
57 std::fs::write(&test_file, "test content").unwrap();
58
59 let result = validator.validate(&test_file);
60 assert!(result.is_ok());
61 }
62
63 #[test]
64 fn test_validate_path_traversal() {
65 let temp_dir = TempDir::new().unwrap();
66 let validator = PathValidator::new(temp_dir.path().to_path_buf());
67
68 let test_file = temp_dir.path().join("test.txt");
70 std::fs::write(&test_file, "content").unwrap();
71
72 let malicious_path = temp_dir.path().join("../etc/passwd");
74 let result = validator.validate(&malicious_path);
75
76 match result {
78 Ok(path) => {
79 if let (Ok(canonical_path), Ok(canonical_root)) = (
82 std::fs::canonicalize(&path),
83 std::fs::canonicalize(temp_dir.path())
84 ) {
85 assert!(canonical_path.starts_with(&canonical_root));
86 }
87 }
88 Err(_) => {
89 assert!(true);
91 }
92 }
93 }
94
95 #[test]
96 fn test_validate_nonexistent_path() {
97 let temp_dir = TempDir::new().unwrap();
98 let validator = PathValidator::new(temp_dir.path().to_path_buf());
99
100 let nonexistent_path = temp_dir.path().join("nonexistent.txt");
101 let result = validator.validate(&nonexistent_path);
102
103 assert!(result.is_ok());
105 }
106
107 #[test]
108 fn test_validate_directory() {
109 let temp_dir = TempDir::new().unwrap();
110 let validator = PathValidator::new(temp_dir.path().to_path_buf());
111
112 let result = validator.validate(temp_dir.path());
113 assert!(result.is_ok());
114 }
115
116 #[test]
117 fn test_validate_nested_path() {
118 let temp_dir = TempDir::new().unwrap();
119 let validator = PathValidator::new(temp_dir.path().to_path_buf());
120
121 let subdir = temp_dir.path().join("subdir");
122 std::fs::create_dir(&subdir).unwrap();
123
124 let nested_file = subdir.join("nested.txt");
125 std::fs::write(&nested_file, "nested").unwrap();
126
127 let result = validator.validate(&nested_file);
128 assert!(result.is_ok());
129 }
130
131 #[test]
132 fn test_validate_absolute_path_outside_root() {
133 let temp_dir = TempDir::new().unwrap();
134 let validator = PathValidator::new(temp_dir.path().to_path_buf());
135
136 let absolute_path = Path::new("/etc/passwd");
137 let result = validator.validate(absolute_path);
138
139 assert!(result.is_err());
141 match result {
142 Err(Error::Forbidden(_)) => assert!(true),
143 _ => panic!("Expected Forbidden error"),
144 }
145 }
146
147 #[test]
148 fn test_validate_same_as_root() {
149 let temp_dir = TempDir::new().unwrap();
150 let validator = PathValidator::new(temp_dir.path().to_path_buf());
151
152 let result = validator.validate(temp_dir.path());
153 assert!(result.is_ok());
154 }
155
156 #[test]
157 fn test_validate_path_validator_clone() {
158 let temp_dir = TempDir::new().unwrap();
159 let validator = PathValidator::new(temp_dir.path().to_path_buf());
160
161 let validator_clone = validator.clone();
162
163 let test_file = temp_dir.path().join("test.txt");
164 std::fs::write(&test_file, "test").unwrap();
165
166 let result = validator_clone.validate(&test_file);
167 assert!(result.is_ok());
168 }
169
170 #[test]
171 fn test_validate_symlink_within_root() {
172 let temp_dir = TempDir::new().unwrap();
173 let validator = PathValidator::new(temp_dir.path().to_path_buf());
174
175 let target_file = temp_dir.path().join("target.txt");
177 std::fs::write(&target_file, "target content").unwrap();
178
179 let symlink = temp_dir.path().join("link.txt");
181 #[cfg(unix)]
182 std::os::unix::fs::symlink(&target_file, &symlink).unwrap();
183
184 let result = validator.validate(&symlink);
186 assert!(result.is_ok());
187 }
188
189 #[test]
190 fn test_validate_nonexistent_file() {
191 let temp_dir = TempDir::new().unwrap();
192 let validator = PathValidator::new(temp_dir.path().to_path_buf());
193
194 let nonexistent = temp_dir.path().join("nonexistent.txt");
196 let result = validator.validate(&nonexistent);
197
198 assert!(result.is_ok());
200 }
201
202 #[test]
203 fn test_validate_path_with_spaces() {
204 let temp_dir = TempDir::new().unwrap();
205 let validator = PathValidator::new(temp_dir.path().to_path_buf());
206
207 let file_with_spaces = temp_dir.path().join("file with spaces.txt");
209 std::fs::write(&file_with_spaces, "content").unwrap();
210
211 let result = validator.validate(&file_with_spaces);
212 assert!(result.is_ok());
213 }
214
215 #[test]
216 fn test_validate_empty_path_components() {
217 let temp_dir = TempDir::new().unwrap();
218 let validator = PathValidator::new(temp_dir.path().to_path_buf());
219
220 let path = temp_dir.path().join("subdir//file.txt");
222 let result = validator.validate(&path);
224 assert!(result.is_ok() || result.is_err());
226 }
227}