bastion_toolkit/
fs_guard.rs1use std::fs::{File, OpenOptions};
8use std::path::{Path, PathBuf};
9use std::io::{Result, Error, ErrorKind};
10use std::os::unix::fs::OpenOptionsExt;
11
12pub struct Jail {
14 root: PathBuf,
15}
16
17impl Jail {
18 pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
20 let root_canonical = root.as_ref().canonicalize()?;
21 if !root_canonical.is_dir() {
22 return Err(Error::new(ErrorKind::InvalidInput, "Jail root must be a directory"));
23 }
24 Ok(Self { root: root_canonical })
25 }
26
27 pub fn open_file<P: AsRef<Path>>(&self, path: P) -> Result<File> {
30 let mut opts = OpenOptions::new();
31 opts.read(true);
32 self.secure_open(path, opts)
33 }
34
35 pub fn create_file<P: AsRef<Path>>(&self, path: P) -> Result<File> {
37 let mut opts = OpenOptions::new();
38 opts.write(true).create(true).truncate(true);
39 self.secure_open(path, opts)
40 }
41
42 fn secure_open<P: AsRef<Path>>(&self, path: P, mut options: OpenOptions) -> Result<File> {
44 let requested_path = path.as_ref();
45
46 let base_path = if requested_path.is_absolute() {
49 requested_path.to_path_buf()
50 } else {
51 self.root.join(requested_path)
52 };
53
54 let full_path = if base_path.exists() {
57 base_path.canonicalize()?
58 } else {
59 match base_path.parent() {
60 Some(parent) if parent.exists() => {
61 let parent_canonical = parent.canonicalize()?;
62 parent_canonical.join(base_path.file_name().unwrap_or_default())
63 }
64 _ => base_path.clone(), }
66 };
67
68 if !full_path.starts_with(&self.root) {
70 return Err(Error::new(ErrorKind::PermissionDenied, "Access Denied: Path outside of jail"));
71 }
72
73 #[cfg(unix)]
76 {
77 options.custom_flags(libc::O_NOFOLLOW);
78 }
79
80 let file = options.open(&full_path)?;
82
83 let metadata = file.metadata()?;
86 if metadata.file_type().is_symlink() {
87 return Err(Error::new(ErrorKind::PermissionDenied, "Access Denied: Symbolic link detected after open"));
88 }
89
90 Ok(file)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100 use std::fs;
101 use tempfile::tempdir;
102
103 #[test]
104 fn test_jail_isolation() -> Result<()> {
105 let dir = tempdir()?;
106 let workspace = dir.path().join("workspace");
107 fs::create_dir(&workspace)?;
108
109 let jail = Jail::new(&workspace)?;
110
111 let safe_file_path = workspace.join("test.txt");
113 fs::write(&safe_file_path, "hello")?;
114 assert!(jail.open_file("test.txt").is_ok());
115
116 assert!(jail.open_file("../outside.txt").is_err());
118
119 assert!(jail.open_file("/etc/passwd").is_err());
121
122 Ok(())
123 }
124
125 #[test]
126 fn test_create_in_jail() -> Result<()> {
127 let dir = tempdir()?;
128 let workspace = dir.path().join("workspace");
129 fs::create_dir(&workspace)?;
130
131 let jail = Jail::new(&workspace)?;
132
133 let res = jail.create_file("new.txt");
135 assert!(res.is_ok());
136
137 let res_evil = jail.create_file("../evil.txt");
139 assert!(res_evil.is_err());
140
141 Ok(())
142 }
143}