fob_graph/analysis/walker/
validation.rs1use std::path::{Path, PathBuf};
7use thiserror::Error;
8
9#[derive(Debug, Error)]
11#[error("Path traversal detected: path '{path}' escapes from cwd '{cwd}'")]
12pub struct PathTraversalError {
13 pub path: PathBuf,
15 pub cwd: PathBuf,
17}
18
19pub fn validate_path_within_cwd(
33 normalized_path: &Path,
34 cwd: &Path,
35) -> Result<(), PathTraversalError> {
36 if let (Ok(normalized), Ok(cwd_normalized)) =
38 (normalized_path.canonicalize(), cwd.canonicalize())
39 {
40 if normalized.starts_with(&cwd_normalized) {
41 return Ok(());
42 }
43 }
44
45 if normalized_path.starts_with(cwd) {
49 let relative = normalized_path
53 .strip_prefix(cwd)
54 .ok()
55 .and_then(|p| p.to_str());
56
57 if let Some(rel) = relative {
58 let dot_dot_count = rel.matches("../").count();
60 if dot_dot_count == 0 {
61 return Ok(());
63 }
64 }
65
66 return Err(PathTraversalError {
70 path: normalized_path.to_path_buf(),
71 cwd: cwd.to_path_buf(),
72 });
73 }
74
75 Err(PathTraversalError {
77 path: normalized_path.to_path_buf(),
78 cwd: cwd.to_path_buf(),
79 })
80}
81
82pub fn normalize_and_validate_path(path: &Path, cwd: &Path) -> Result<PathBuf, PathTraversalError> {
98 use path_clean::PathClean;
99
100 let normalized = if path.is_absolute() {
101 path.to_path_buf()
102 } else {
103 cwd.join(path)
104 };
105
106 let cleaned = normalized.clean();
107
108 validate_path_within_cwd(&cleaned, cwd)?;
110
111 Ok(cleaned)
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use std::fs;
118 use tempfile::TempDir;
119
120 #[test]
121 fn test_validate_path_within_cwd_valid() {
122 let temp_dir = TempDir::new().unwrap();
123 let cwd = temp_dir.path();
124 let valid_path = cwd.join("src").join("index.ts");
125
126 fs::create_dir_all(valid_path.parent().unwrap()).unwrap();
128 fs::write(&valid_path, "").unwrap();
129
130 assert!(validate_path_within_cwd(&valid_path, cwd).is_ok());
131 }
132
133 #[test]
134 fn test_validate_path_within_cwd_traversal() {
135 let temp_dir = TempDir::new().unwrap();
136 let cwd = temp_dir.path();
137 let traversal_path = cwd.join("..").join("etc").join("passwd");
138
139 let result = validate_path_within_cwd(&traversal_path, cwd);
140 assert!(result.is_err());
141 if let Err(PathTraversalError { path, cwd: _ }) = result {
142 assert_eq!(path, traversal_path);
143 }
144 }
145
146 #[test]
147 fn test_normalize_and_validate_relative_path() {
148 let temp_dir = TempDir::new().unwrap();
149 let cwd = temp_dir.path();
150 let relative_path = Path::new("src/index.ts");
151 let expected = cwd.join("src").join("index.ts");
152
153 let result = normalize_and_validate_path(relative_path, cwd).unwrap();
154 assert_eq!(result, expected);
155 }
156
157 #[test]
158 fn test_normalize_and_validate_traversal_attempt() {
159 let temp_dir = TempDir::new().unwrap();
160 let cwd = temp_dir.path();
161 let traversal_path = Path::new("../../../etc/passwd");
162
163 let result = normalize_and_validate_path(traversal_path, cwd);
164 assert!(result.is_err());
165 }
166
167 #[test]
168 fn test_normalize_and_validate_with_dot_components() {
169 let temp_dir = TempDir::new().unwrap();
170 let cwd = temp_dir.path();
171 let path_with_dots = Path::new("./src/../src/./index.ts");
172 let expected = cwd.join("src").join("index.ts");
173
174 let result = normalize_and_validate_path(path_with_dots, cwd).unwrap();
175 assert_eq!(result, expected);
176 }
177}