Skip to main content

relay_core_lib/utils/
path.rs

1use std::path::{Path, PathBuf};
2use std::io;
3
4/// PathSanitizer ensures that file access is restricted to a specific root directory.
5#[derive(Debug, Clone)]
6pub struct PathSanitizer {
7    root: PathBuf,
8}
9
10impl PathSanitizer {
11    pub fn new(root: PathBuf) -> Self {
12        // We try to canonicalize the root. If it fails (doesn't exist), we keep it as is.
13        // Ideally the root should exist.
14        let canonical_root = root.canonicalize().unwrap_or(root);
15        Self { 
16            root: canonical_root
17        }
18    }
19
20    /// Resolve a relative path against the sandbox root, ensuring it stays within the root.
21    /// Returns the absolute canonicalized path if safe.
22    pub fn sanitize(&self, path_str: &str) -> io::Result<PathBuf> {
23        let path = Path::new(path_str);
24        
25        // If path is absolute, we check if it is within root.
26        // If relative, we join with root.
27        let candidate = if path.is_absolute() {
28             path.to_path_buf()
29        } else {
30             self.root.join(path)
31        };
32
33        // Canonicalize to resolve .. and symlinks.
34        // Note: canonicalize() requires the file to exist.
35        // For read operations (BodySource::File, MapLocal), this is expected.
36        let canonical = candidate.canonicalize()?;
37
38        if canonical.starts_with(&self.root) {
39            Ok(canonical)
40        } else {
41            Err(io::Error::new(io::ErrorKind::PermissionDenied, format!("Path traversal detected: {:?}", canonical)))
42        }
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use std::fs;
50    use tempfile::TempDir;
51
52    #[test]
53    fn test_sandbox_allows_valid_file() {
54        let temp_dir = TempDir::new().unwrap();
55        let file_path = temp_dir.path().join("test.txt");
56        fs::write(&file_path, "content").unwrap();
57
58        let sanitizer = PathSanitizer::new(temp_dir.path().to_path_buf());
59        let result = sanitizer.sanitize("test.txt");
60        
61        assert!(result.is_ok());
62        assert_eq!(result.unwrap(), file_path.canonicalize().unwrap());
63    }
64
65    #[test]
66    fn test_sandbox_denies_traversal() {
67        let temp_dir = TempDir::new().unwrap();
68        let sanitizer = PathSanitizer::new(temp_dir.path().to_path_buf());
69        
70        // Try to access /etc/passwd or something outside
71        // We use ".." to go up
72        let _result = sanitizer.sanitize("../outside.txt");
73        // Since outside.txt doesn't exist, canonicalize might fail with NotFound, 
74        // which is also acceptable as "denied" in a sense, but we want to test boundary.
75        
76        // Better: create a file outside
77        let outside_dir = TempDir::new().unwrap();
78        let outside_file = outside_dir.path().join("outside.txt");
79        fs::write(&outside_file, "secret").unwrap();
80
81        // This assumes temp dirs are separate
82        let result = sanitizer.sanitize(outside_file.to_str().unwrap());
83        
84        assert!(result.is_err());
85    }
86}