use std::fs::File;
use std::io;
use std::path::Path;
use crate::error::Error;
pub fn open_read_no_traverse(path: &Path) -> Result<File, Error> {
open_read_no_traverse_inner(path).map_err(|e| match e {
OpenError::Refused(msg) => Error::PolicyTampered(msg),
OpenError::Io(io_err) => Error::StorageIo(io_err),
})
}
enum OpenError {
Refused(String),
Io(io::Error),
}
#[cfg(unix)]
fn open_read_no_traverse_inner(path: &Path) -> Result<File, OpenError> {
use std::os::unix::fs::OpenOptionsExt;
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC)
.open(path)
.map_err(|e| {
if e.raw_os_error() == Some(libc::ELOOP) {
OpenError::Refused(format!(
"refusing to open '{}': path is a symlink (TOCTOU-free check)",
path.display()
))
} else {
OpenError::Io(e)
}
})?;
Ok(file)
}
#[cfg(windows)]
fn open_read_no_traverse_inner(path: &Path) -> Result<File, OpenError> {
use std::os::windows::fs::OpenOptionsExt;
use std::os::windows::io::AsRawHandle;
use windows_sys::Win32::Storage::FileSystem::{
GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION, FILE_ATTRIBUTE_REPARSE_POINT,
FILE_FLAG_OPEN_REPARSE_POINT,
};
let file = std::fs::OpenOptions::new()
.read(true)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(path)
.map_err(OpenError::Io)?;
let mut info: BY_HANDLE_FILE_INFORMATION = unsafe { std::mem::zeroed() };
let ok = unsafe {
GetFileInformationByHandle(file.as_raw_handle().cast::<core::ffi::c_void>(), &mut info)
};
if ok == 0 {
return Err(OpenError::Io(io::Error::last_os_error()));
}
if (info.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 {
return Err(OpenError::Refused(format!(
"refusing to open '{}': path is an NTFS reparse point (junction, mount, \
symlink, cloud-file). Reparse points silently redirect path resolution \
and are not allowed in vault / .envseal paths.",
path.display()
)));
}
Ok(file)
}
#[cfg(not(any(unix, windows)))]
fn open_read_no_traverse_inner(path: &Path) -> Result<File, OpenError> {
let meta = std::fs::symlink_metadata(path).map_err(OpenError::Io)?;
if meta.file_type().is_symlink() {
return Err(OpenError::Refused(format!(
"refusing to open '{}': path is a symlink",
path.display()
)));
}
std::fs::File::open(path).map_err(OpenError::Io)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn opens_a_regular_file() {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("plain.txt");
std::fs::write(&path, b"hello").unwrap();
let f = open_read_no_traverse(&path);
assert!(f.is_ok(), "regular file open must succeed: {:?}", f.err());
}
#[cfg(unix)]
#[test]
fn refuses_a_symlink() {
let tmp = tempfile::tempdir().unwrap();
let target = tmp.path().join("real.txt");
std::fs::write(&target, b"x").unwrap();
let link = tmp.path().join("link.txt");
std::os::unix::fs::symlink(&target, &link).unwrap();
let err = open_read_no_traverse(&link).expect_err("must refuse symlink");
match err {
Error::PolicyTampered(_) => {}
other => panic!("expected PolicyTampered, got {other:?}"),
}
}
}