Skip to main content

relay_core_lib/utils/
path.rs

1use std::io;
2use std::path::{Path, PathBuf};
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(
42                io::ErrorKind::PermissionDenied,
43                format!("Path traversal detected: {:?}", canonical),
44            ))
45        }
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use std::fs;
53    use tempfile::TempDir;
54
55    #[test]
56    fn test_sandbox_allows_valid_file() {
57        let temp_dir = TempDir::new().unwrap();
58        let file_path = temp_dir.path().join("test.txt");
59        fs::write(&file_path, "content").unwrap();
60
61        let sanitizer = PathSanitizer::new(temp_dir.path().to_path_buf());
62        let result = sanitizer.sanitize("test.txt");
63
64        assert!(result.is_ok());
65        assert_eq!(result.unwrap(), file_path.canonicalize().unwrap());
66    }
67
68    #[test]
69    fn test_sandbox_denies_traversal() {
70        let temp_dir = TempDir::new().unwrap();
71        let sanitizer = PathSanitizer::new(temp_dir.path().to_path_buf());
72
73        // Try to access /etc/passwd or something outside
74        // We use ".." to go up
75        let _result = sanitizer.sanitize("../outside.txt");
76        // Since outside.txt doesn't exist, canonicalize might fail with NotFound,
77        // which is also acceptable as "denied" in a sense, but we want to test boundary.
78
79        // Better: create a file outside
80        let outside_dir = TempDir::new().unwrap();
81        let outside_file = outside_dir.path().join("outside.txt");
82        fs::write(&outside_file, "secret").unwrap();
83
84        // This assumes temp dirs are separate
85        let result = sanitizer.sanitize(outside_file.to_str().unwrap());
86
87        assert!(result.is_err());
88    }
89}