#[cfg(all(feature = "virtual-path", unix))]
use crate::VirtualRoot;
#[cfg(all(
feature = "virtual-path",
any(unix, all(windows, feature = "junctions"))
))]
use crate::{PathBoundary, StrictPathError};
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_append_through_symlink_escape_is_blocked() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let restriction_dir = base.join("boundary");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&restriction_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let outside_file = outside_dir.join("sensitive.log");
std::fs::write(&outside_file, "original content\n").unwrap();
let link_inside = restriction_dir.join("log.txt");
unixfs::symlink(&outside_file, &link_inside).unwrap();
let restricted_dir: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let result = restricted_dir.strict_join("log.txt");
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(strict_path) => {
let append_result = strict_path.append("malicious log entry\n");
if append_result.is_ok() {
let outside_content = std::fs::read_to_string(&outside_file).unwrap();
assert_eq!(
outside_content, "original content\n",
"SECURITY FAILURE: append() modified file outside boundary via symlink"
);
}
}
Err(other) => panic!("Unexpected error: {other:?}"),
}
let final_content = std::fs::read_to_string(&outside_file).unwrap();
assert_eq!(
final_content, "original content\n",
"Outside file must remain unmodified"
);
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_create_file_through_symlink_escape_is_blocked() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let restriction_dir = base.join("boundary");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&restriction_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let link_to_outside = restriction_dir.join("escape_dir");
unixfs::symlink(&outside_dir, &link_to_outside).unwrap();
let restricted_dir: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let result = restricted_dir.strict_join("escape_dir/new_file.txt");
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(strict_path) => {
let create_result = strict_path.create_file();
if create_result.is_ok() {
assert!(
!outside_dir.join("new_file.txt").exists(),
"SECURITY FAILURE: create_file() created file outside boundary"
);
}
}
Err(other) => panic!("Unexpected error: {other:?}"),
}
assert!(
!outside_dir.join("new_file.txt").exists(),
"No file should be created outside boundary"
);
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_open_file_through_symlink_escape_is_blocked() {
use std::io::Read;
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let restriction_dir = base.join("boundary");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&restriction_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let sensitive_file = outside_dir.join("secrets.txt");
std::fs::write(&sensitive_file, "API_KEY=supersecret123").unwrap();
let link_inside = restriction_dir.join("config.txt");
unixfs::symlink(&sensitive_file, &link_inside).unwrap();
let restricted_dir: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let result = restricted_dir.strict_join("config.txt");
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(strict_path) => {
match strict_path.open_file() {
Ok(mut file) => {
let mut contents = String::new();
let _ = file.read_to_string(&mut contents);
assert!(
!contents.contains("supersecret123"),
"SECURITY FAILURE: open_file() leaked sensitive data via symlink"
);
}
Err(_) => {
}
}
}
Err(other) => panic!("Unexpected error: {other:?}"),
}
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_write_through_preexisting_malicious_symlink() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let restriction_dir = base.join("boundary");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&restriction_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let target_outside = outside_dir.join("important.cfg");
std::fs::write(&target_outside, "original config\n").unwrap();
let poison_link = restriction_dir.join("config.cfg");
unixfs::symlink(&target_outside, &poison_link).unwrap();
let restricted_dir: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let join_result = restricted_dir.strict_join("config.cfg");
match join_result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(strict_path) => {
let write_result = strict_path.write("malicious config\n");
if write_result.is_ok() {
let outside_content = std::fs::read_to_string(&target_outside).unwrap();
assert_eq!(
outside_content, "original config\n",
"SECURITY FAILURE: write() modified file outside boundary via symlink"
);
}
}
Err(other) => panic!("Unexpected error: {other:?}"),
}
let final_content = std::fs::read_to_string(&target_outside).unwrap();
assert_eq!(
final_content, "original config\n",
"Outside file must remain unmodified"
);
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_virtual_append_through_symlink_clamps_or_blocks() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let vroot_dir = base.join("vroot");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&vroot_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let outside_log = outside_dir.join("system.log");
std::fs::write(&outside_log, "system log\n").unwrap();
let link_inside = vroot_dir.join("user.log");
unixfs::symlink(&outside_log, &link_inside).unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new(&vroot_dir).unwrap();
match vroot.virtual_join("user.log") {
Ok(vpath) => {
let _ = vpath.append("user content\n");
let outside_content = std::fs::read_to_string(&outside_log).unwrap();
assert_eq!(
outside_content, "system log\n",
"VirtualPath must not allow writes outside virtual root"
);
}
Err(_) => {
}
}
}
#[cfg(feature = "virtual-path")]
#[cfg(feature = "junctions")]
#[test]
#[cfg(windows)]
fn test_append_through_junction_escape_is_blocked() {
let td = tempfile::tempdir().unwrap();
let base = td.path();
let restriction_dir = base.join("boundary");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&restriction_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let outside_file = outside_dir.join("sensitive.log");
std::fs::write(&outside_file, "original content\n").unwrap();
let junction_inside = restriction_dir.join("escape");
junction::create(&outside_dir, &junction_inside).expect("junction creation");
let restricted_dir: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let result = restricted_dir.strict_join("escape/sensitive.log");
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(strict_path) => {
let append_result = strict_path.append("malicious log entry\n");
if append_result.is_ok() {
let outside_content = std::fs::read_to_string(&outside_file).unwrap();
assert_eq!(
outside_content, "original content\n",
"SECURITY FAILURE: append() modified file outside boundary via junction"
);
}
}
Err(other) => panic!("Unexpected error: {other:?}"),
}
let final_content = std::fs::read_to_string(&outside_file).unwrap();
assert_eq!(
final_content, "original content\n",
"Outside file must remain unmodified"
);
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_read_through_symlink_escape_is_blocked() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let base = td.path();
let restriction_dir = base.join("boundary");
let outside_dir = base.join("outside");
std::fs::create_dir_all(&restriction_dir).unwrap();
std::fs::create_dir_all(&outside_dir).unwrap();
let sensitive_file = outside_dir.join("passwd");
std::fs::write(&sensitive_file, "root:x:0:0:root:/root:/bin/bash").unwrap();
let link_inside = restriction_dir.join("data.txt");
unixfs::symlink(&sensitive_file, &link_inside).unwrap();
let restricted_dir: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let result = restricted_dir.strict_join("data.txt");
match result {
Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(strict_path) => {
match strict_path.read_to_string() {
Ok(contents) => {
assert!(
!contents.contains("root:x:0:0"),
"SECURITY FAILURE: read() leaked sensitive data via symlink"
);
}
Err(_) => {
}
}
}
Err(other) => panic!("Unexpected error: {other:?}"),
}
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_circular_symlink_produces_clean_error() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let restriction_dir = td.path().join("boundary");
std::fs::create_dir_all(&restriction_dir).unwrap();
let link_a = restriction_dir.join("link_a");
let link_b = restriction_dir.join("link_b");
unixfs::symlink(&link_b, &link_a).unwrap();
unixfs::symlink(&link_a, &link_b).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
let vroot: VirtualRoot<()> = VirtualRoot::try_new(&restriction_dir).unwrap();
match restriction.strict_join("link_a") {
Err(StrictPathError::PathResolutionError { .. })
| Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(_) => panic!("Circular symlink must not succeed silently"),
Err(other) => panic!("Unexpected error for circular symlink: {other:?}"),
}
match vroot.virtual_join("link_a") {
Err(StrictPathError::PathResolutionError { .. })
| Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(vpath) => {
assert!(
vpath
.as_unvirtual()
.strictpath_starts_with(vroot.interop_path()),
"Circular symlink must remain within boundary if accepted"
);
}
Err(other) => panic!("Unexpected virtual error for circular symlink: {other:?}"),
}
}
#[cfg(feature = "virtual-path")]
#[test]
#[cfg(unix)]
fn test_self_referencing_symlink_produces_clean_error() {
use std::os::unix::fs as unixfs;
let td = tempfile::tempdir().unwrap();
let restriction_dir = td.path().join("boundary");
std::fs::create_dir_all(&restriction_dir).unwrap();
let self_link = restriction_dir.join("loop");
unixfs::symlink(&self_link, &self_link).unwrap();
let restriction: PathBoundary = PathBoundary::try_new(&restriction_dir).unwrap();
match restriction.strict_join("loop") {
Err(StrictPathError::PathResolutionError { .. })
| Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(_) => panic!("Self-referencing symlink must not succeed silently"),
Err(other) => panic!("Unexpected error for self-link: {other:?}"),
}
}
#[cfg(feature = "virtual-path")]
#[cfg(feature = "junctions")]
#[test]
#[cfg(windows)]
fn test_circular_junction_produces_clean_error() {
let td = tempfile::tempdir().unwrap();
let restriction_dir = td.path().join("boundary");
std::fs::create_dir_all(&restriction_dir).unwrap();
let dir_a = restriction_dir.join("dir_a");
let dir_b = restriction_dir.join("dir_b");
std::fs::create_dir_all(&dir_b).unwrap();
match junction::create(&dir_b, &dir_a) {
Ok(_) => {
std::fs::remove_dir_all(&dir_b).ok();
match junction::create(&dir_a, &dir_b) {
Ok(_) => {
let restriction: PathBoundary =
PathBoundary::try_new(&restriction_dir).unwrap();
match restriction.strict_join("dir_a/subfile.txt") {
Err(StrictPathError::PathResolutionError { .. })
| Err(StrictPathError::PathEscapesBoundary { .. }) => {
}
Ok(_) => {
}
Err(other) => {
panic!("Unexpected error for circular junction: {other:?}")
}
}
}
Err(junction_err) => {
eprintln!("Note: Could not create second circular junction: {junction_err}; skipping test");
}
}
}
Err(junction_err) => {
eprintln!(
"Note: Could not create first circular junction: {junction_err}; skipping test"
);
}
}
}