Skip to main content

agent_sandbox/fs/
capability.rs

1use std::path::{Path, PathBuf};
2
3use crate::error::{Result, SandboxError};
4
5/// Validate that a path resolves within the allowed root directory.
6/// Prevents path traversal attacks (e.g., `../../etc/passwd`).
7pub fn validate_path(root: &Path, requested: &str) -> Result<PathBuf> {
8    let root = root.canonicalize().map_err(SandboxError::Io)?;
9
10    let full_path = root.join(requested);
11
12    // Resolve the path (handles `..`, `.`, symlinks)
13    let resolved = if full_path.exists() {
14        full_path.canonicalize().map_err(SandboxError::Io)?
15    } else {
16        // For paths that don't exist yet, normalize manually
17        normalize_path(&full_path)
18    };
19
20    if !resolved.starts_with(&root) {
21        return Err(SandboxError::PathTraversal(format!(
22            "'{}' escapes sandbox root '{}'",
23            requested,
24            root.display()
25        )));
26    }
27
28    Ok(resolved)
29}
30
31/// Normalize a path without requiring it to exist on disk.
32fn normalize_path(path: &Path) -> PathBuf {
33    let mut result = PathBuf::new();
34
35    for component in path.components() {
36        match component {
37            std::path::Component::ParentDir => {
38                result.pop();
39            }
40            std::path::Component::CurDir => {}
41            other => result.push(other),
42        }
43    }
44
45    result
46}
47
48#[cfg(test)]
49mod tests {
50    use super::*;
51
52    #[test]
53    fn test_valid_path() {
54        let tmp = tempfile::tempdir().unwrap();
55        let root = tmp.path();
56
57        // Create a file inside root
58        std::fs::write(root.join("test.txt"), "hello").unwrap();
59
60        let result = validate_path(root, "test.txt");
61        assert!(result.is_ok());
62    }
63
64    #[test]
65    fn test_path_traversal_blocked() {
66        let tmp = tempfile::tempdir().unwrap();
67        let root = tmp.path();
68
69        let result = validate_path(root, "../../../etc/passwd");
70        assert!(result.is_err());
71        assert!(matches!(
72            result.unwrap_err(),
73            SandboxError::PathTraversal(_)
74        ));
75    }
76
77    #[test]
78    fn test_nested_path() {
79        let tmp = tempfile::tempdir().unwrap();
80        let root = tmp.path();
81        std::fs::create_dir_all(root.join("a/b")).unwrap();
82        std::fs::write(root.join("a/b/c.txt"), "content").unwrap();
83
84        let result = validate_path(root, "a/b/c.txt");
85        assert!(result.is_ok());
86    }
87
88    #[test]
89    fn test_nonexistent_path_within_root() {
90        let tmp = tempfile::tempdir().unwrap();
91        let root = tmp.path();
92
93        let result = validate_path(root, "new_file.txt");
94        assert!(result.is_ok());
95    }
96}