use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
use std::io::{Result, Error, ErrorKind};
use std::os::unix::fs::OpenOptionsExt;
pub struct Jail {
root: PathBuf,
}
impl Jail {
pub fn new<P: AsRef<Path>>(root: P) -> Result<Self> {
let root_canonical = root.as_ref().canonicalize()?;
if !root_canonical.is_dir() {
return Err(Error::new(ErrorKind::InvalidInput, "Jail root must be a directory"));
}
Ok(Self { root: root_canonical })
}
pub fn open_file<P: AsRef<Path>>(&self, path: P) -> Result<File> {
let mut opts = OpenOptions::new();
opts.read(true);
self.secure_open(path, opts)
}
pub fn create_file<P: AsRef<Path>>(&self, path: P) -> Result<File> {
let mut opts = OpenOptions::new();
opts.write(true).create(true).truncate(true);
self.secure_open(path, opts)
}
fn secure_open<P: AsRef<Path>>(&self, path: P, mut options: OpenOptions) -> Result<File> {
let requested_path = path.as_ref();
let base_path = if requested_path.is_absolute() {
requested_path.to_path_buf()
} else {
self.root.join(requested_path)
};
let full_path = if base_path.exists() {
base_path.canonicalize()?
} else {
match base_path.parent() {
Some(parent) if parent.exists() => {
let parent_canonical = parent.canonicalize()?;
parent_canonical.join(base_path.file_name().unwrap_or_default())
}
_ => base_path.clone(), }
};
if !full_path.starts_with(&self.root) {
return Err(Error::new(ErrorKind::PermissionDenied, "Access Denied: Path outside of jail"));
}
#[cfg(unix)]
{
options.custom_flags(libc::O_NOFOLLOW);
}
let file = options.open(&full_path)?;
let metadata = file.metadata()?;
if metadata.file_type().is_symlink() {
return Err(Error::new(ErrorKind::PermissionDenied, "Access Denied: Symbolic link detected after open"));
}
Ok(file)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::tempdir;
#[test]
fn test_jail_isolation() -> Result<()> {
let dir = tempdir()?;
let workspace = dir.path().join("workspace");
fs::create_dir(&workspace)?;
let jail = Jail::new(&workspace)?;
let safe_file_path = workspace.join("test.txt");
fs::write(&safe_file_path, "hello")?;
assert!(jail.open_file("test.txt").is_ok());
assert!(jail.open_file("../outside.txt").is_err());
assert!(jail.open_file("/etc/passwd").is_err());
Ok(())
}
#[test]
fn test_create_in_jail() -> Result<()> {
let dir = tempdir()?;
let workspace = dir.path().join("workspace");
fs::create_dir(&workspace)?;
let jail = Jail::new(&workspace)?;
let res = jail.create_file("new.txt");
assert!(res.is_ok());
let res_evil = jail.create_file("../evil.txt");
assert!(res_evil.is_err());
Ok(())
}
}