synwire_core/security/
path.rs1use crate::error::{SynwireError, ToolError};
4
5pub 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 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}