oxi_agent/tools/
path_security.rs1use std::path::{Path, PathBuf};
4
5#[derive(Debug)]
7pub enum PathSecurityError {
8 NotFound(PathBuf),
10 Traversal(PathBuf),
12 OutsideWorkspace(PathBuf),
14}
15
16impl std::fmt::Display for PathSecurityError {
17 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18 match self {
19 Self::NotFound(p) => write!(f, "Path not found: {}", p.display()),
20 Self::Traversal(p) => write!(f, "Path traversal detected: {}", p.display()),
21 Self::OutsideWorkspace(p) => write!(f, "Path outside workspace: {}", p.display()),
22 }
23 }
24}
25
26impl std::error::Error for PathSecurityError {}
27
28pub struct PathGuard {
30 root: PathBuf,
31}
32
33impl PathGuard {
34 pub fn new(cwd: &Path) -> Self {
36 let root = cwd.canonicalize().unwrap_or_else(|_| cwd.to_path_buf());
37 Self { root }
38 }
39
40 pub fn validate(&self, path: &Path) -> Result<PathBuf, PathSecurityError> {
42 if path.components().any(|c| c.as_os_str() == "..") {
44 return Err(PathSecurityError::Traversal(path.to_path_buf()));
45 }
46
47 if path.exists() {
49 let canonical = path
50 .canonicalize()
51 .map_err(|_| PathSecurityError::NotFound(path.to_path_buf()))?;
52
53 if !canonical.starts_with(&self.root) {
55 return Err(PathSecurityError::OutsideWorkspace(canonical));
56 }
57
58 Ok(canonical)
59 } else {
60 Ok(path.to_path_buf())
62 }
63 }
64
65 pub fn validate_traversal(&self, path: &Path) -> Result<PathBuf, PathSecurityError> {
72 if path.components().any(|c| c.as_os_str() == "..") {
74 return Err(PathSecurityError::Traversal(path.to_path_buf()));
75 }
76
77 if path.exists() {
79 let canonical = path
80 .canonicalize()
81 .map_err(|_| PathSecurityError::NotFound(path.to_path_buf()))?;
82 Ok(canonical)
83 } else {
84 Ok(path.to_path_buf())
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use std::fs;
93
94 #[test]
95 fn reject_traversal() {
96 let tmp = tempfile::tempdir().unwrap();
97 let guard = PathGuard::new(tmp.path());
98 let result = guard.validate(Path::new("../../../etc/passwd"));
99 assert!(result.is_err());
100 }
101
102 #[test]
103 fn accept_valid_path() {
104 let tmp = tempfile::tempdir().unwrap();
105 let test_file = tmp.path().join("test.txt");
106 fs::write(&test_file, "hello").unwrap();
107 let guard = PathGuard::new(tmp.path());
108 let result = guard.validate(&test_file);
109 assert!(result.is_ok());
110 }
111
112 #[test]
113 fn reject_absolute_outside() {
114 let tmp = tempfile::tempdir().unwrap();
115 let guard = PathGuard::new(tmp.path());
116 let result = guard.validate(Path::new("/etc/passwd"));
117 assert!(result.is_err());
118 }
119}