Skip to main content

runner_core/
path_safety.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Context, Result};
4
5/// Normalize a user-supplied path and ensure it stays within an allowed root.
6/// Rejects absolute candidates and any that escape the root via `..`.
7pub fn normalize_under_root(root: &Path, candidate: &Path) -> Result<PathBuf> {
8    if candidate.is_absolute() {
9        anyhow::bail!("absolute paths are not allowed: {}", candidate.display());
10    }
11
12    let root = root
13        .canonicalize()
14        .with_context(|| format!("failed to canonicalize {}", root.display()))?;
15    let joined = root.join(candidate);
16    let canon = joined
17        .canonicalize()
18        .with_context(|| format!("failed to canonicalize {}", joined.display()))?;
19
20    if !canon.starts_with(&root) {
21        anyhow::bail!(
22            "path escapes root ({}): {}",
23            root.display(),
24            canon.display()
25        );
26    }
27
28    Ok(canon)
29}
30
31#[cfg(test)]
32mod tests {
33    use super::normalize_under_root;
34    use anyhow::Result;
35    use std::fs;
36    use tempfile::TempDir;
37
38    #[test]
39    fn normalizes_relative_roots_before_comparing() -> Result<()> {
40        let current_dir = std::env::current_dir()?;
41        let temp = tempfile::tempdir_in(&current_dir)?;
42        let relative_root = temp.path().strip_prefix(&current_dir)?;
43        let file_path = temp.path().join("pack.gtpack");
44        fs::write(&file_path, b"pack")?;
45
46        let normalized = normalize_under_root(relative_root, std::path::Path::new("pack.gtpack"))?;
47
48        assert_eq!(normalized, file_path.canonicalize()?);
49        Ok(())
50    }
51
52    #[test]
53    fn rejects_parent_escape() -> Result<()> {
54        let temp = TempDir::new()?;
55        let sibling = temp
56            .path()
57            .parent()
58            .expect("tempdir parent")
59            .join("escape.gtpack");
60        fs::write(&sibling, b"escape")?;
61
62        let err = normalize_under_root(temp.path(), std::path::Path::new("../escape.gtpack"))
63            .expect_err("parent traversal should be rejected");
64
65        assert!(err.to_string().contains("path escapes root"));
66        Ok(())
67    }
68}