aether/sandbox/
path_validator.rs

1//! 路径安全验证器
2//!
3//! 提供统一的路径安全检查,防止路径遍历攻击(`..`)和越权访问。
4
5use std::collections::HashSet;
6use std::path::{Component, Path, PathBuf};
7
8/// 路径验证错误
9#[derive(Debug, Clone, PartialEq)]
10pub enum PathValidationError {
11    /// 路径超出根目录限制
12    OutsideRoot { path: PathBuf, root: PathBuf },
13    /// 绝对路径被禁止
14    AbsolutePathNotAllowed(PathBuf),
15    /// 父目录遍历 `..` 被禁止
16    ParentTraversalNotAllowed(PathBuf),
17    /// 文件扩展名不在白名单中
18    ExtensionNotAllowed { path: PathBuf, extension: String },
19    /// 路径解析失败
20    InvalidPath(PathBuf),
21}
22
23impl std::fmt::Display for PathValidationError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        match self {
26            PathValidationError::OutsideRoot { path, root } => {
27                write!(
28                    f,
29                    "Path '{}' is outside allowed root '{}'",
30                    path.display(),
31                    root.display()
32                )
33            }
34            PathValidationError::AbsolutePathNotAllowed(path) => {
35                write!(f, "Absolute path '{}' not allowed", path.display())
36            }
37            PathValidationError::ParentTraversalNotAllowed(path) => {
38                write!(
39                    f,
40                    "Parent traversal '..' not allowed in path '{}'",
41                    path.display()
42                )
43            }
44            PathValidationError::ExtensionNotAllowed { path, extension } => {
45                write!(
46                    f,
47                    "File extension '{}' not allowed for path '{}'",
48                    extension,
49                    path.display()
50                )
51            }
52            PathValidationError::InvalidPath(path) => {
53                write!(f, "Invalid path '{}'", path.display())
54            }
55        }
56    }
57}
58
59impl std::error::Error for PathValidationError {}
60
61/// 路径限制规则
62#[derive(Debug, Clone)]
63pub struct PathRestriction {
64    /// 根目录(路径必须在此之下)
65    pub root_dir: PathBuf,
66    /// 是否允许绝对路径
67    pub allow_absolute: bool,
68    /// 是否允许 `..` 路径遍历
69    pub allow_parent_traversal: bool,
70    /// 允许的文件扩展名白名单(None 表示不限制)
71    pub allowed_extensions: Option<HashSet<String>>,
72}
73
74impl Default for PathRestriction {
75    fn default() -> Self {
76        Self {
77            root_dir: PathBuf::from("."),
78            allow_absolute: false,
79            allow_parent_traversal: false,
80            allowed_extensions: None,
81        }
82    }
83}
84
85/// 路径验证器
86#[derive(Clone)]
87pub struct PathValidator {
88    restriction: PathRestriction,
89}
90
91impl PathValidator {
92    /// 创建新的路径验证器
93    pub fn new(restriction: PathRestriction) -> Self {
94        Self { restriction }
95    }
96
97    /// 从根目录创建验证器(默认严格配置)
98    pub fn with_root_dir(root_dir: PathBuf) -> Self {
99        Self::new(PathRestriction {
100            root_dir,
101            allow_absolute: false,
102            allow_parent_traversal: false,
103            allowed_extensions: None,
104        })
105    }
106
107    /// 验证并规范化路径
108    ///
109    /// 返回规范化的绝对路径,如果验证失败则返回错误
110    pub fn validate_and_normalize(&self, path: &Path) -> Result<PathBuf, PathValidationError> {
111        // 1. 检查绝对路径
112        if path.is_absolute() && !self.restriction.allow_absolute {
113            return Err(PathValidationError::AbsolutePathNotAllowed(
114                path.to_path_buf(),
115            ));
116        }
117
118        // 2. 检查父目录遍历(快速路径检查)
119        if !self.restriction.allow_parent_traversal {
120            let path_str = path.to_string_lossy();
121            if path_str.contains("..") {
122                return Err(PathValidationError::ParentTraversalNotAllowed(
123                    path.to_path_buf(),
124                ));
125            }
126        }
127
128        // 3. 规范化路径(解析 . 和 ..)
129        let normalized = self.canonicalize_safe(path)?;
130
131        // 4. 检查是否在根目录下
132        if let Ok(root) = self.restriction.root_dir.canonicalize()
133            && !normalized.starts_with(&root)
134        {
135            return Err(PathValidationError::OutsideRoot {
136                path: normalized.clone(),
137                root,
138            });
139        }
140
141        // 5. 检查文件扩展名
142        if let Some(allowed) = &self.restriction.allowed_extensions
143            && let Some(ext) = normalized.extension()
144        {
145            let ext_str = ext.to_string_lossy().to_lowercase();
146            if !allowed.contains(&ext_str) {
147                return Err(PathValidationError::ExtensionNotAllowed {
148                    path: normalized.clone(),
149                    extension: ext_str,
150                });
151            }
152        }
153
154        Ok(normalized)
155    }
156
157    /// 安全地规范化路径(避免 IO 错误导致的 panic)
158    fn canonicalize_safe(&self, path: &Path) -> Result<PathBuf, PathValidationError> {
159        // 对于相对路径,基于 root_dir 解析
160        let full_path = if path.is_absolute() {
161            path.to_path_buf()
162        } else {
163            self.restriction.root_dir.join(path)
164        };
165
166        // 尝试 canonicalize,如果失败则手动清理路径组件
167        match full_path.canonicalize() {
168            Ok(canon) => Ok(canon),
169            Err(_) => {
170                // 文件不存在时,手动规范化路径组件
171                let mut result = PathBuf::new();
172                for component in full_path.components() {
173                    match component {
174                        Component::Prefix(_) | Component::RootDir | Component::Normal(_) => {
175                            result.push(component);
176                        }
177                        Component::CurDir => {
178                            // 忽略 .
179                        }
180                        Component::ParentDir => {
181                            if !self.restriction.allow_parent_traversal {
182                                return Err(PathValidationError::ParentTraversalNotAllowed(
183                                    full_path,
184                                ));
185                            }
186                            // 尝试弹出父目录
187                            if !result.pop() {
188                                return Err(PathValidationError::InvalidPath(full_path));
189                            }
190                        }
191                    }
192                }
193                Ok(result)
194            }
195        }
196    }
197
198    /// 检查路径是否有效(不进行规范化,仅快速检查)
199    pub fn is_valid(&self, path: &Path) -> bool {
200        self.validate_and_normalize(path).is_ok()
201    }
202
203    /// 获取路径限制配置
204    pub fn restriction(&self) -> &PathRestriction {
205        &self.restriction
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn test_path_validator_blocks_parent_traversal() {
215        let restriction = PathRestriction {
216            root_dir: PathBuf::from("/safe"),
217            allow_absolute: false,
218            allow_parent_traversal: false,
219            allowed_extensions: None,
220        };
221
222        let validator = PathValidator::new(restriction);
223
224        // 应该被阻止
225        assert!(
226            validator
227                .validate_and_normalize(Path::new("../etc/passwd"))
228                .is_err()
229        );
230        assert!(
231            validator
232                .validate_and_normalize(Path::new("safe/../../etc/passwd"))
233                .is_err()
234        );
235    }
236
237    #[test]
238    fn test_path_validator_blocks_absolute_paths() {
239        let restriction = PathRestriction {
240            root_dir: PathBuf::from("/safe"),
241            allow_absolute: false,
242            allow_parent_traversal: false,
243            allowed_extensions: None,
244        };
245
246        let validator = PathValidator::new(restriction);
247
248        // 应该被阻止
249        assert!(
250            validator
251                .validate_and_normalize(Path::new("/etc/passwd"))
252                .is_err()
253        );
254    }
255
256    #[test]
257    fn test_path_validator_extension_whitelist() {
258        let mut allowed = HashSet::new();
259        allowed.insert("aether".to_string());
260        allowed.insert("txt".to_string());
261
262        let restriction = PathRestriction {
263            root_dir: PathBuf::from("/safe"),
264            allow_absolute: false,
265            allow_parent_traversal: false,
266            allowed_extensions: Some(allowed),
267        };
268
269        let _validator = PathValidator::new(restriction);
270
271        // 创建临时文件进行测试
272        use std::fs;
273        let temp_dir = std::env::temp_dir();
274        let test_file = temp_dir.join("test.aether");
275        fs::write(&test_file, "test").unwrap();
276
277        // 应该被允许(基于当前目录的相对路径)
278        // 注意:这个测试可能需要根据实际文件系统调整
279    }
280
281    #[test]
282    fn test_path_validator_with_root_dir() {
283        let validator = PathValidator::with_root_dir(PathBuf::from("/tmp/test"));
284
285        // 相对路径应该基于 root_dir
286        let result = validator.validate_and_normalize(Path::new("subdir/file.txt"));
287        // 由于 /tmp/test/subdir/file.txt 不存在,会手动规范化
288        assert!(result.is_ok() || result.is_err()); // 取决于文件系统
289    }
290
291    #[test]
292    fn test_path_error_display() {
293        let err = PathValidationError::OutsideRoot {
294            path: PathBuf::from("/etc/passwd"),
295            root: PathBuf::from("/safe"),
296        };
297        assert!(err.to_string().contains("outside allowed root"));
298
299        let err = PathValidationError::AbsolutePathNotAllowed(PathBuf::from("/etc/passwd"));
300        assert!(err.to_string().contains("not allowed"));
301
302        let err = PathValidationError::ParentTraversalNotAllowed(PathBuf::from("../file"));
303        assert!(err.to_string().contains("Parent traversal"));
304    }
305}