use std::path::Path;
pub fn harden_owner_only(path: &Path) {
#[cfg(target_os = "windows")]
harden_windows_acl(path);
#[cfg(not(target_os = "windows"))]
let _ = path;
}
#[cfg(target_os = "windows")]
fn harden_windows_acl(path: &Path) {
use std::process::Command;
let path_str = match path.to_str() {
Some(s) => s,
None => {
tracing::warn!(?path, "ACL hardening skipped: path is not valid UTF-8");
return;
}
};
let run_icacls = |args: &[&str]| -> std::io::Result<()> {
let status = Command::new("icacls").args(args).status()?;
if status.success() {
Ok(())
} else {
Err(std::io::Error::other(format!(
"icacls {args:?} exited with status {status}"
)))
}
};
let _ = run_icacls(&[path_str, "/grant:r", "*S-1-5-18:F"]);
let user_sid = match current_process_sid_string() {
Ok(sid) => sid,
Err(error) => {
tracing::warn!(
?error,
?path,
"ACL hardening stopped after the SYSTEM grant: the current process SID could \
not be read, so inherited ACEs are left in place"
);
return;
}
};
if let Err(error) = run_icacls(&[path_str, "/grant:r", &format!("*{user_sid}:F")]) {
tracing::warn!(
?error,
?path,
"ACL hardening stopped after the SYSTEM grant: the process SID grant failed, so \
inherited ACEs are left in place"
);
return;
}
if let Err(e) = run_icacls(&[path_str, "/inheritance:r"]) {
tracing::warn!(?e, ?path, "ACL hardening: /inheritance:r failed");
}
let _ = run_icacls(&[path_str, "/remove:g", "*S-1-5-32-544"]);
let _ = run_icacls(&[path_str, "/remove:d", "*S-1-5-32-544"]);
let owner = format!("*{user_sid}");
if let Err(e) = run_icacls(&[path_str, "/setowner", &owner]) {
tracing::warn!(?e, ?path, "ACL hardening: /setowner failed");
}
}
#[cfg(target_os = "windows")]
fn current_process_sid_string() -> std::io::Result<String> {
use windows::core::PWSTR;
use windows::Win32::Foundation::{CloseHandle, LocalFree, HANDLE, HLOCAL};
use windows::Win32::Security::Authorization::ConvertSidToStringSidW;
use windows::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER};
use windows::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken};
let windows_error = |error: windows::core::Error| std::io::Error::other(error.to_string());
let mut token = HANDLE::default();
unsafe { OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &mut token) }
.map_err(windows_error)?;
let result = (|| {
let mut needed = 0u32;
let _ = unsafe { GetTokenInformation(token, TokenUser, None, 0, &mut needed) };
if needed == 0 {
return Err(std::io::Error::last_os_error());
}
let words = (needed as usize).div_ceil(std::mem::size_of::<usize>());
let mut buffer = vec![0usize; words];
unsafe {
GetTokenInformation(
token,
TokenUser,
Some(buffer.as_mut_ptr().cast()),
needed,
&mut needed,
)
}
.map_err(windows_error)?;
let user = unsafe { &*buffer.as_ptr().cast::<TOKEN_USER>() };
let mut sid_text = PWSTR::null();
unsafe { ConvertSidToStringSidW(user.User.Sid, &mut sid_text) }.map_err(windows_error)?;
let sid = unsafe { sid_text.to_string() }
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error));
unsafe {
let _ = LocalFree(HLOCAL(sid_text.0.cast()));
}
sid
})();
let _ = unsafe { CloseHandle(token) };
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn harden_is_a_noop_on_a_missing_path_and_never_panics() {
harden_owner_only(Path::new("this/path/does/not/exist/xyz"));
}
}