use crate::error::{Error, Result};
use std::fs::{File, OpenOptions};
use std::path::{Path, PathBuf};
pub fn safe_file_path(path: &Path, allow_symlinks: bool) -> Result<PathBuf> {
if path.exists() {
if path.is_symlink() {
if !allow_symlinks {
return Err(Error::Validation(format!(
"Path {} is a symlink, which is not allowed",
path.display()
)));
}
let target = std::fs::read_link(path)?;
if !is_safe_symlink_target(&target) {
return Err(Error::Validation(format!(
"Symlink target {} is not in an allowed location",
target.display()
)));
}
return Ok(target);
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let metadata = std::fs::metadata(path)?;
if metadata.nlink() > 1 {
return Err(Error::Validation(format!(
"Path {} has multiple hard links ({}), which may be a security risk",
path.display(),
metadata.nlink()
)));
}
}
}
Ok(path.to_path_buf())
}
fn is_safe_symlink_target(target: &Path) -> bool {
if let Ok(canonical) = target.canonicalize() {
canonical.starts_with("/tmp") || canonical.starts_with("/var/app/data")
} else {
false
}
}
pub fn safe_open_file(path: &Path, allow_symlinks: bool) -> Result<File> {
let safe_path = safe_file_path(path, allow_symlinks)?;
File::open(&safe_path).map_err(Error::from)
}
pub fn safe_create_file(path: &Path, allow_symlinks: bool) -> Result<File> {
let safe_path = safe_file_path(path, allow_symlinks)?;
File::create(&safe_path).map_err(Error::from)
}
pub fn safe_open_options(path: &Path, allow_symlinks: bool) -> Result<OpenOptions> {
let _safe_path = safe_file_path(path, allow_symlinks)?;
Ok(OpenOptions::new())
}