Skip to main content

rust_serv/path_security/
validator.rs

1use crate::error::{Error, Result};
2use std::path::{Path, PathBuf};
3
4/// Path validator to prevent directory traversal attacks
5#[derive(Clone)]
6pub struct PathValidator {
7    root: PathBuf,
8}
9
10impl PathValidator {
11    /// Create a new path validator
12    pub fn new(root: PathBuf) -> Self {
13        Self { root }
14    }
15
16    /// Validate and normalize a path
17    pub fn validate(&self, path: &Path) -> Result<PathBuf> {
18        // Get canonical root
19        let canonical_root = std::fs::canonicalize(&self.root)
20            .map_err(|e| Error::PathSecurity(format!("Failed to canonicalize root: {}", e)))?;
21
22        // Canonicalize the path to resolve .. and symlinks
23        // If file doesn't exist, we'll use the path as-is after normalization
24        let canonical_path = match std::fs::canonicalize(path) {
25            Ok(p) => p,
26            Err(e) => {
27                // If file doesn't exist, still validate the parent directory
28                if e.kind() == std::io::ErrorKind::NotFound {
29                    // Try to normalize the path without resolving
30                    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        // Check if path is within root
38        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        // Create a real file to test path traversal detection
69        let test_file = temp_dir.path().join("test.txt");
70        std::fs::write(&test_file, "content").unwrap();
71
72        // Create a path with .. that escapes the temp directory
73        let malicious_path = temp_dir.path().join("../etc/passwd");
74        let result = validator.validate(&malicious_path);
75
76        // The path should be either an error or point to something within the temp dir
77        match result {
78            Ok(path) => {
79                // If it succeeds, ensure it's still within bounds
80                // Use canonicalize for both paths to ensure proper comparison
81                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                // Or it should error for security reasons
90                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        // Should succeed even for nonexistent files
104        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        // Should return Forbidden error
140        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        // Create a file
176        let target_file = temp_dir.path().join("target.txt");
177        std::fs::write(&target_file, "target content").unwrap();
178
179        // Create a symlink within root
180        let symlink = temp_dir.path().join("link.txt");
181        #[cfg(unix)]
182        std::os::unix::fs::symlink(&target_file, &symlink).unwrap();
183
184        // Validate symlink
185        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        // Path that doesn't exist
195        let nonexistent = temp_dir.path().join("nonexistent.txt");
196        let result = validator.validate(&nonexistent);
197
198        // Should succeed with normalized path even if file doesn't exist
199        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        // Create file with spaces in name
208        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        // Path with empty components (multiple slashes)
221        let path = temp_dir.path().join("subdir//file.txt");
222        // This test verifies that paths with empty components are handled
223        let result = validator.validate(&path);
224        // Should succeed or fail gracefully
225        assert!(result.is_ok() || result.is_err());
226    }
227}