Skip to main content

synwire_core/security/
path.rs

1//! Path traversal protection for tool file operations.
2
3use crate::error::{SynwireError, ToolError};
4
5/// Validate a path for safety (no traversal, no null bytes, no absolute paths).
6///
7/// This is a defence-in-depth measure to prevent tools from accessing files
8/// outside their intended sandbox.
9///
10/// # Errors
11///
12/// Returns [`SynwireError::Tool`] with [`ToolError::PathTraversal`] if the path:
13/// - Contains null bytes
14/// - Contains `..` path traversal components
15/// - Starts with `/` or `\` (absolute path)
16/// - Contains Windows-style drive letters (e.g. `C:`)
17pub fn validate_tool_path(path: &str) -> Result<(), SynwireError> {
18    if path.contains('\0') {
19        return Err(SynwireError::Tool(ToolError::PathTraversal {
20            path: path.into(),
21        }));
22    }
23    if path.contains("..") {
24        return Err(SynwireError::Tool(ToolError::PathTraversal {
25            path: path.into(),
26        }));
27    }
28    if path.starts_with('/') || path.starts_with('\\') {
29        return Err(SynwireError::Tool(ToolError::PathTraversal {
30            path: path.into(),
31        }));
32    }
33    // Reject Windows drive letters like C: or D:
34    if path.len() >= 2 && path.as_bytes()[0].is_ascii_alphabetic() && path.as_bytes()[1] == b':' {
35        return Err(SynwireError::Tool(ToolError::PathTraversal {
36            path: path.into(),
37        }));
38    }
39    Ok(())
40}
41
42#[cfg(test)]
43#[allow(clippy::unwrap_used)]
44mod tests {
45    use super::*;
46
47    #[test]
48    fn allows_safe_relative_paths() {
49        validate_tool_path("foo/bar.txt").unwrap();
50        validate_tool_path("data").unwrap();
51        validate_tool_path("a/b/c/d.json").unwrap();
52    }
53
54    #[test]
55    fn rejects_null_bytes() {
56        let err = validate_tool_path("foo\0bar").unwrap_err();
57        assert!(err.to_string().contains("path traversal"));
58    }
59
60    #[test]
61    fn rejects_dot_dot() {
62        let err = validate_tool_path("../etc/passwd").unwrap_err();
63        assert!(err.to_string().contains("path traversal"));
64    }
65
66    #[test]
67    fn rejects_embedded_dot_dot() {
68        let err = validate_tool_path("foo/../../bar").unwrap_err();
69        assert!(err.to_string().contains("path traversal"));
70    }
71
72    #[test]
73    fn rejects_absolute_unix() {
74        let err = validate_tool_path("/etc/passwd").unwrap_err();
75        assert!(err.to_string().contains("path traversal"));
76    }
77
78    #[test]
79    fn rejects_absolute_windows() {
80        let err = validate_tool_path("\\Windows\\System32").unwrap_err();
81        assert!(err.to_string().contains("path traversal"));
82    }
83
84    #[test]
85    fn rejects_drive_letter() {
86        let err = validate_tool_path("C:\\Windows").unwrap_err();
87        assert!(err.to_string().contains("path traversal"));
88    }
89}