1use crate::types::BuildError;
13use std::path::{Path, PathBuf};
14
15pub fn contain_within(base: &Path, relative: &Path) -> Result<PathBuf, BuildError> {
23 let base_canon = base
24 .canonicalize()
25 .map_err(|e| BuildError::Io(format!("canonicalize base {}: {e}", base.display())))?;
26 let joined = base_canon.join(relative);
27 let canon = joined
28 .canonicalize()
29 .map_err(|e| BuildError::Io(format!("canonicalize {}: {e}", joined.display())))?;
30 if !canon.starts_with(&base_canon) {
31 return Err(BuildError::Io(format!(
32 "path {} escapes the node checkout {} (symlink traversal?)",
33 canon.display(),
34 base_canon.display()
35 )));
36 }
37 Ok(canon)
38}
39
40#[cfg(test)]
41mod tests {
42 use super::*;
43
44 #[test]
45 fn accepts_inner_path() {
46 let dir = tempfile::tempdir().unwrap();
47 std::fs::create_dir(dir.path().join("sub")).unwrap();
48 let got = contain_within(dir.path(), Path::new("sub")).unwrap();
49 assert!(got.ends_with("sub"));
50 }
51
52 #[test]
53 fn rejects_missing_path() {
54 let dir = tempfile::tempdir().unwrap();
55 assert!(contain_within(dir.path(), Path::new("absent")).is_err());
56 }
57
58 #[cfg(unix)]
59 #[test]
60 fn rejects_symlink_escape() {
61 let outside = tempfile::tempdir().unwrap();
62 let dir = tempfile::tempdir().unwrap();
63 std::os::unix::fs::symlink(outside.path(), dir.path().join("evil")).unwrap();
64 let err = contain_within(dir.path(), Path::new("evil")).unwrap_err();
65 assert!(err.to_string().contains("escapes"), "got: {err}");
66 }
67
68 #[cfg(unix)]
69 #[test]
70 fn rejects_symlinked_file_escape() {
71 let outside = tempfile::tempdir().unwrap();
72 let secret = outside.path().join("secret");
73 std::fs::write(&secret, b"x").unwrap();
74 let dir = tempfile::tempdir().unwrap();
75 std::os::unix::fs::symlink(&secret, dir.path().join("Dockerfile")).unwrap();
76 let err = contain_within(dir.path(), Path::new("Dockerfile")).unwrap_err();
77 assert!(err.to_string().contains("escapes"), "got: {err}");
78 }
79}