Skip to main content

path_rs/
containment.rs

1//! Lexical root containment checks.
2//!
3//! # Security warning
4//!
5//! Lexical containment does **not** account for symlinks. A path that is
6//! lexically inside a root may still resolve outside it via symlink traversal.
7//! Do not use these helpers as a sole security boundary for untrusted paths.
8
9use crate::error::PathError;
10use crate::internal::components::starts_with_components;
11use crate::internal::validation::reject_nul_path;
12use crate::normalize::normalize;
13use std::path::{Path, PathBuf};
14
15/// Returns `true` if `path` is lexically equal to or beneath `root`.
16///
17/// Both paths should already be normalized for reliable results. Prefer
18/// [`ensure_inside`] which normalizes first.
19///
20/// # Filesystem access
21///
22/// **No.**
23pub fn is_lexically_inside(path: &Path, root: &Path) -> bool {
24    if path == root {
25        return true;
26    }
27    starts_with_components(path, root)
28    // Ensure we match on component boundaries, not as a string prefix of a longer name.
29    // `starts_with_components` already compares whole components.
30}
31
32/// Normalize both paths and ensure `path` stays inside `root`.
33///
34/// Returns the normalized path on success.
35///
36/// # Filesystem access
37///
38/// **No.**
39///
40/// # Security
41///
42/// Not symlink-safe. See module documentation.
43pub fn ensure_inside(root: impl AsRef<Path>, path: impl AsRef<Path>) -> Result<PathBuf, PathError> {
44    let root = normalize(root.as_ref())?;
45    let path = normalize(path.as_ref())?;
46    reject_nul_path(&root)?;
47    reject_nul_path(&path)?;
48
49    if is_lexically_inside(&path, &root) {
50        Ok(path)
51    } else {
52        Err(PathError::root_escape(path))
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn inside_and_escape() {
62        let root = Path::new("/repo");
63        assert!(is_lexically_inside(Path::new("/repo/src"), root));
64        assert!(is_lexically_inside(Path::new("/repo"), root));
65        assert!(!is_lexically_inside(Path::new("/etc"), root));
66        assert!(!is_lexically_inside(Path::new("/repo2"), root));
67    }
68}