use crate::soft_canonicalize;
#[cfg(feature = "anchored")]
use crate::anchored_canonicalize;
#[cfg(unix)]
#[test]
fn test_symlink_swap_race_resistance() {
use std::fs;
use std::os::unix::fs::symlink;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let safe_target = base.join("safe");
let dangerous_target = base.join("dangerous");
fs::create_dir(&safe_target).unwrap();
fs::create_dir(&dangerous_target).unwrap();
let link = base.join("racing_link");
symlink(&safe_target, &link).unwrap();
let link_clone = link.clone();
let dangerous_clone = dangerous_target.clone();
let handle = thread::spawn(move || {
thread::sleep(Duration::from_micros(100));
let _ = fs::remove_file(&link_clone);
let _ = symlink(&dangerous_clone, &link_clone);
});
let result = soft_canonicalize(link.join("nonexistent.txt"));
handle.join().unwrap();
match result {
Ok(resolved) => {
let safe_expected = soft_canonicalize(safe_target.join("nonexistent.txt")).unwrap();
let dangerous_expected =
soft_canonicalize(dangerous_target.join("nonexistent.txt")).unwrap();
assert!(
resolved == safe_expected || resolved == dangerous_expected,
"Should resolve to one of the race targets.\nExpected either:\n {:?}\n {:?}\nGot:\n {:?}",
safe_expected,
dangerous_expected,
resolved
);
}
Err(e) => {
assert!(
e.kind() == std::io::ErrorKind::NotFound
|| e.kind() == std::io::ErrorKind::InvalidInput,
"Should fail gracefully: {}",
e
);
}
}
}
#[cfg(unix)]
#[test]
fn test_proc_self_fd_symlink_attack() {
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let base = temp_dir.path();
let malicious_link = base.join("innocent_looking_path");
symlink("/proc/self/fd/7", &malicious_link).unwrap();
let result = soft_canonicalize(&malicious_link);
if let Ok(resolved) = result {
let resolved_str = resolved.to_string_lossy();
println!("Resolved /proc/self/fd/7 symlink to: {}", resolved_str);
}
}
#[cfg(feature = "anchored")]
#[cfg(unix)]
#[test]
fn test_anchored_symlink_toctou_race() {
use std::fs;
use std::os::unix::fs::symlink;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let anchor = temp_dir.path().join("anchor");
fs::create_dir_all(&anchor).unwrap();
let internal_target = anchor.join("internal");
fs::create_dir(internal_target).unwrap();
let link = anchor.join("link");
symlink("internal", &link).unwrap();
let link_clone = link;
let handle = thread::spawn(move || {
thread::sleep(Duration::from_micros(100));
let _ = fs::remove_file(&link_clone);
let _ = symlink("/etc/passwd", &link_clone);
});
let result = anchored_canonicalize(&anchor, "link/nonexistent");
handle.join().unwrap();
if let Ok(resolved) = result {
assert!(
resolved.starts_with(&anchor)
|| resolved.starts_with(fs::canonicalize(&anchor).unwrap()),
"Race should not escape anchor: {:?}",
resolved
);
}
}
#[cfg(feature = "anchored")]
#[cfg(unix)]
#[test]
fn test_configmap_style_symlink_escape_prevention() {
use std::fs;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let container_root = temp_dir.path().join("container");
fs::create_dir_all(&container_root).unwrap();
let configmap_dir = container_root.join("configmap");
fs::create_dir(&configmap_dir).unwrap();
let malicious_config = configmap_dir.join("escape_link");
symlink("/etc/shadow", malicious_config).unwrap();
let result = anchored_canonicalize(&container_root, "configmap/escape_link");
assert!(result.is_ok());
let resolved = result.unwrap();
let canon_root = fs::canonicalize(&container_root).unwrap();
assert!(
resolved.starts_with(&canon_root),
"ConfigMap symlink should be clamped to container: {:?} not in {:?}",
resolved,
canon_root
);
assert!(
resolved.ends_with("etc/shadow"),
"Should clamp to virtual etc/shadow: {:?}",
resolved
);
}
#[cfg(feature = "anchored")]
#[cfg(unix)]
#[test]
fn test_nested_configmap_symlinks() {
use std::fs;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let container = temp_dir.path().join("container");
fs::create_dir_all(&container).unwrap();
let config_dir = container.join("config");
fs::create_dir(&config_dir).unwrap();
let link1 = config_dir.join("link1");
let link2 = config_dir.join("link2");
symlink("link2", link1).unwrap();
symlink("/host/sensitive/data", link2).unwrap();
let result = anchored_canonicalize(&container, "config/link1");
assert!(result.is_ok());
let resolved = result.unwrap();
let canon_container = fs::canonicalize(&container).unwrap();
assert!(
resolved.starts_with(canon_container),
"Chained symlinks should stay clamped: {:?}",
resolved
);
}
#[cfg(feature = "anchored")]
#[test]
fn test_tar_bomb_style_dotdot_sequences() {
use std::fs;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let extract_zone = temp_dir.path().join("extract");
fs::create_dir_all(&extract_zone).unwrap();
let malicious_paths = vec![
"../../../etc/cron.d/backdoor",
"../../../../../../../../tmp/evil.sh",
"subdir/../../../../../../etc/passwd",
];
for malicious_path in malicious_paths {
let result = anchored_canonicalize(&extract_zone, malicious_path);
if let Ok(resolved) = result {
assert!(
resolved.starts_with(soft_canonicalize(&extract_zone).unwrap()),
"Archive path should be contained: {} -> {:?}",
malicious_path,
resolved
);
}
}
}
#[cfg(feature = "anchored")]
#[cfg(unix)]
#[test]
fn test_tar_bomb_with_absolute_symlinks() {
use std::fs;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let extract_zone = temp_dir.path().join("extract");
fs::create_dir_all(&extract_zone).unwrap();
let extracted_link = extract_zone.join("tmplink");
symlink("/tmp", extracted_link).unwrap();
let result = anchored_canonicalize(&extract_zone, "tmplink/evil_payload.sh");
assert!(result.is_ok());
let resolved = result.unwrap();
let canon_extract = fs::canonicalize(&extract_zone).unwrap();
assert!(
resolved.starts_with(canon_extract),
"Archive symlink should be clamped: {:?}",
resolved
);
}
#[test]
fn test_url_encoded_path_traversal() {
let paths = vec![
"..%2F..%2F..%2Fetc%2Fpasswd", "..%5c..%5c..%5cwindows", "%2e%2e%2f%2e%2e%2f", ];
for path in paths {
let result = soft_canonicalize(path);
if let Ok(resolved) = result {
let resolved_str = resolved.to_string_lossy();
assert!(!resolved_str.is_empty());
}
}
}
#[test]
fn test_double_encoded_traversal() {
let paths = vec![
"%252e%252e%252f", "%252e%252e%255c", ];
for path in paths {
let result = soft_canonicalize(path);
match result {
Ok(_) | Err(_) => {
}
}
}
}
#[cfg(feature = "anchored")]
#[cfg(unix)]
#[test]
fn test_combined_device_symlink_dotdot_attack() {
use std::fs;
use std::os::unix::fs::symlink;
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let anchor = temp_dir.path().join("sandbox");
fs::create_dir_all(&anchor).unwrap();
let attack_dir = anchor.join("attack");
fs::create_dir(&attack_dir).unwrap();
let link = attack_dir.join("escape");
symlink("/etc/passwd", link).unwrap();
let result = anchored_canonicalize(&anchor, "attack/escape/../../../etc/shadow");
assert!(result.is_ok());
let resolved = result.unwrap();
let canon_anchor = fs::canonicalize(&anchor).unwrap();
assert!(
resolved.starts_with(canon_anchor),
"Combined attack should be contained: {:?}",
resolved
);
}
#[test]
fn test_extremely_long_path_with_traversal() {
let mut long_path = String::new();
for _ in 0..1000 {
long_path.push_str("../");
}
long_path.push_str("etc/passwd");
let result = soft_canonicalize(&long_path);
assert!(result.is_ok());
}
#[test]
fn test_deeply_nested_nonexistent_path() {
let mut deep_path = String::from("a");
for _ in 0..500 {
deep_path.push_str("/b");
}
let result = soft_canonicalize(&deep_path);
assert!(result.is_ok());
}
#[cfg(feature = "anchored")]
#[test]
fn test_anchored_with_empty_components() {
use tempfile::TempDir;
let temp_dir = TempDir::new().unwrap();
let anchor = temp_dir.path();
let paths = vec!["foo//bar", "foo///bar", "./foo//bar", "foo/./bar"];
for path in paths {
let result = anchored_canonicalize(anchor, path);
assert!(
result.is_ok(),
"Empty components should be normalized: {}",
path
);
}
}