#![cfg(windows)]
use crate::{PathBoundary, StrictPathError};
use std::path::Path;
struct MaliciousZipStructure {
top_dir: String,
link_name: String,
symlink_target: String,
payload_file: String,
}
impl MaliciousZipStructure {
fn new_desktop_attack(username: &str) -> Self {
Self {
top_dir: "data".to_string(),
link_name: "link_in".to_string(),
symlink_target: format!("C:\\Users\\{username}\\Desktop"),
payload_file: "malicious.exe".to_string(),
}
}
fn new_system32_attack() -> Self {
Self {
top_dir: "data".to_string(),
link_name: "link_in".to_string(),
symlink_target: "C:\\Windows\\System32".to_string(),
payload_file: "malicious.dll".to_string(),
}
}
fn new_relative_traversal_attack() -> Self {
Self {
top_dir: "data".to_string(),
link_name: "link_in".to_string(),
symlink_target: "..\\..\\..\\sensitive".to_string(),
payload_file: "payload.txt".to_string(),
}
}
}
#[test]
fn test_cve_2025_11001_desktop_symlink_attack_blocked() {
let attack = MaliciousZipStructure::new_desktop_attack("TestUser");
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
let top_dir_path = extraction_sandbox.strict_join(&attack.top_dir).unwrap();
top_dir_path.create_dir().unwrap();
let top_dir = &attack.top_dir;
let link_name = &attack.link_name;
let link_path = extraction_sandbox
.strict_join(format!("{top_dir}/{link_name}"))
.unwrap();
let symlink_target_result = extraction_sandbox.strict_join(&attack.symlink_target);
assert!(
symlink_target_result.is_err(),
"strict-path MUST reject absolute path to Desktop"
);
match symlink_target_result {
Err(StrictPathError::PathEscapesBoundary { attempted_path, .. }) => {
let path_str = attempted_path.to_string_lossy();
assert!(
path_str.contains("Users") || path_str.contains("Desktop"),
"Error should reference the attempted Desktop path, got: {path_str}"
);
}
Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(_) => panic!("strict_join MUST NOT accept absolute path to C:\\Users"),
Err(other) => panic!("Unexpected error variant: {other:?}"),
}
let safe_target_dir = extraction_sandbox
.strict_join(format!("{}/safe_target_dir", attack.top_dir))
.unwrap();
safe_target_dir.create_dir_all().unwrap();
match safe_target_dir.strict_symlink(link_path.interop_path()) {
Ok(_) => {
assert!(link_path.exists(), "Link should exist after creation");
}
Err(e) if e.raw_os_error() == Some(1314) => {
link_path.create_parent_dir_all().ok();
#[cfg(feature = "junctions")]
{
match safe_target_dir.strict_junction(link_path.interop_path()) {
Ok(_) => {
if let Err(err) = link_path.read_dir() {
eprintln!("Warning: Junction created but not readable as dir: {err:?}");
}
}
Err(err) => {
eprintln!(
"Note: Could not create junction via built-in helper after symlink privilege error: {err:?}"
);
}
}
}
#[cfg(not(feature = "junctions"))]
{
panic!(
"This test verifies the junction fallback path but the 'junctions' feature is disabled.\n\
Enable it with: cargo test -p strict-path --features junctions (CI/dev runs use --all-features)."
);
}
}
Err(e) => panic!("Unexpected error creating symlink: {e}"),
}
let top_dir = &attack.top_dir;
let link_name = &attack.link_name;
let payload_file = &attack.payload_file;
let payload_through_link =
extraction_sandbox.strict_join(format!("{top_dir}/{link_name}/{payload_file}"));
match payload_through_link {
Ok(payload_path) => {
assert!(
payload_path.strictpath_starts_with(extraction_sandbox.interop_path()),
"Payload path must remain within extraction boundary"
);
match payload_path.write(b"fake malware") {
Ok(_) => {
let desktop_path = Path::new(&attack.symlink_target).join(&attack.payload_file);
assert!(
!desktop_path.exists(),
"Desktop must remain untouched even after successful write"
);
}
Err(e) => {
eprintln!("Write blocked by OS: {e}");
}
}
}
Err(e) => {
eprintln!("Attack blocked: strict_join rejected path through symlink: {e}");
let desktop_path = Path::new(&attack.symlink_target).join(&attack.payload_file);
assert!(
!desktop_path.exists(),
"Desktop must remain untouched - attack successfully blocked"
);
}
}
}
#[test]
fn test_cve_2025_11001_issue2_prepended_directory_bypass_blocked() {
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
let nested_dir = extraction_sandbox
.strict_join("data/subdir/nested")
.unwrap();
nested_dir.create_dir_all().unwrap();
let absolute_targets = &[
"C:\\Users\\Public\\Desktop",
"C:\\Windows\\System32",
"C:\\ProgramData\\sensitive.txt",
];
for &target in absolute_targets {
let result = extraction_sandbox.strict_join(format!("data/subdir/{target}"));
assert!(
result.is_err(),
"strict-path MUST reject absolute path even with prepended directory: {target}"
);
match result {
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(_) => {
panic!("Prepended directory MUST NOT bypass absolute path detection: {target}")
}
Err(other) => panic!("Unexpected error for '{target}': {other:?}"),
}
}
for &target in absolute_targets {
let target_result = extraction_sandbox.strict_join(target);
assert!(
target_result.is_err(),
"Symlink target validation MUST reject absolute paths: {target}"
);
}
}
#[test]
fn test_cve_2025_11001_system32_attack_blocked() {
let attack = MaliciousZipStructure::new_system32_attack();
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
let symlink_target_result = extraction_sandbox.strict_join(&attack.symlink_target);
assert!(
symlink_target_result.is_err(),
"strict-path MUST reject absolute path to System32"
);
match symlink_target_result {
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(_) => panic!("strict_join MUST NOT accept absolute path to C:\\Windows\\System32"),
Err(other) => panic!("Unexpected error variant: {other:?}"),
}
}
#[test]
fn test_cve_2025_11001_forward_slash_absolute_windows_paths_blocked() {
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
let absolute_targets = vec!["C:/Windows/System32", "C:/Users/Public/Desktop"];
for target in absolute_targets {
let result = extraction_sandbox.strict_join(target);
assert!(
result.is_err(),
"strict-path MUST reject forward-slash absolute path: {target}"
);
match result {
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(p) => panic!("strict_join MUST NOT accept forward-slash absolute path: {p:?}"),
Err(other) => panic!("Unexpected error variant: {other:?}"),
}
}
}
#[test]
fn test_cve_2025_11001_relative_traversal_blocked() {
let attack = MaliciousZipStructure::new_relative_traversal_attack();
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
let top_dir_path = extraction_sandbox.strict_join(&attack.top_dir).unwrap();
top_dir_path.create_dir().unwrap();
let symlink_target_result = extraction_sandbox.strict_join(&attack.symlink_target);
assert!(
symlink_target_result.is_err(),
"strict-path MUST reject relative path traversal"
);
match symlink_target_result {
Err(StrictPathError::PathEscapesBoundary { attempted_path, .. }) => {
let path_str = attempted_path.to_string_lossy();
assert!(
path_str.contains("..") || path_str.contains("sensitive"),
"Error should reference the attempted traversal: {path_str}"
);
}
Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(_) => panic!("strict_join MUST NOT accept path with parent directory components"),
Err(other) => panic!("Unexpected error variant: {other:?}"),
}
}
#[test]
fn test_cve_2025_11001_unc_path_attack_blocked() {
let unc_targets = vec![
"\\\\malicious-server\\share\\payload.exe",
"\\\\192.168.1.100\\c$\\Windows\\System32",
"//network-share/sensitive/data.db",
];
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
for unc_path in unc_targets {
let result = extraction_sandbox.strict_join(unc_path);
assert!(
result.is_err(),
"strict-path MUST reject UNC path: {unc_path}"
);
match result {
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {
}
Ok(_) => panic!("strict_join MUST NOT accept UNC path: {unc_path}"),
Err(other) => panic!("Unexpected error for UNC path '{unc_path}': {other:?}"),
}
}
}
#[test]
fn test_cve_2025_11001_mixed_encoding_attack_blocked() {
let encoded_attacks = vec![
"..%2F..%2F..%2FUsers%2FPublic",
"..%5C..%5C..%5CWindows%5CSystem32",
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd", ];
let extraction_dir = tempfile::tempdir().unwrap();
let extraction_sandbox: PathBoundary =
PathBoundary::try_new_create(extraction_dir.path()).unwrap();
for encoded_path in encoded_attacks {
let result = extraction_sandbox.strict_join(encoded_path);
match result {
Ok(validated_path) => {
assert!(
validated_path.strictpath_starts_with(extraction_sandbox.interop_path()),
"Even literal encoded string must stay within boundary"
);
}
Err(StrictPathError::PathEscapesBoundary { .. })
| Err(StrictPathError::PathResolutionError { .. }) => {
}
Err(other) => panic!("Unexpected error for encoded path '{encoded_path}': {other:?}"),
}
}
}