#[cfg(feature = "virtual-path")]
use crate::VirtualRoot;
#[cfg(all(windows, feature = "virtual-path"))]
fn symlink_permission_denied(err: &std::io::Error) -> bool {
const ERROR_PRIVILEGE_NOT_HELD: i32 = 1314;
err.kind() == std::io::ErrorKind::PermissionDenied
|| err.raw_os_error() == Some(ERROR_PRIVILEGE_NOT_HELD)
}
#[cfg(all(not(windows), feature = "virtual-path"))]
fn symlink_permission_denied(_err: &std::io::Error) -> bool {
false
}
#[test]
#[cfg(feature = "virtual-path")]
fn following_symlink_pointing_outside_vroot() {
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path()).unwrap();
let system_td = tempfile::tempdir().unwrap();
let external_target = system_td.path().join("external_secret.txt");
std::fs::write(&external_target, b"system secret").unwrap();
let symlink_path = td.path().join("malicious_link.txt");
#[cfg(unix)]
let symlink_result = std::os::unix::fs::symlink(&external_target, &symlink_path);
#[cfg(windows)]
let symlink_result = std::os::windows::fs::symlink_file(&external_target, &symlink_path);
if let Err(err) = symlink_result {
if symlink_permission_denied(&err) {
eprintln!("Skipping symlink clamping test due to missing privileges: {err:?}");
return;
}
panic!("Failed to create test symlink: {err:?}");
}
let vpath = vroot
.virtual_join("malicious_link.txt")
.expect("Symlink should be resolved with clamping");
let canonical_vroot = std::fs::canonicalize(td.path()).unwrap();
assert!(
vpath.as_unvirtual().strictpath_starts_with(&canonical_vroot),
"Symlink target MUST be clamped within virtual root.\nGot: {:?}\nVRoot: {canonical_vroot:?}\nOriginal target: {external_target:?}",
vpath.as_unvirtual().strictpath_display()
);
#[cfg(unix)]
{
let external_stripped = external_target
.strip_prefix("/")
.unwrap_or(&external_target);
let expected_clamped = canonical_vroot.join(external_stripped);
assert_eq!(
vpath.as_unvirtual().strictpath_display().to_string(),
expected_clamped.to_string_lossy(),
"Clamped path should preserve original absolute path structure within vroot"
);
}
#[cfg(windows)]
{
assert!(
vpath
.as_unvirtual()
.strictpath_starts_with(&canonical_vroot),
"Clamped path should be within vroot on Windows"
);
}
let read_result = vpath.read_to_string();
assert!(
read_result.is_err(),
"Reading clamped symlink should fail (file doesn't exist at clamped location)"
);
}
#[test]
#[cfg(all(windows, feature = "virtual-path"))]
fn following_junction_pointing_outside_vroot() {
use std::process::Command;
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path()).unwrap();
let system_td = tempfile::tempdir().unwrap();
let external_target = system_td.path().join("external_secrets");
std::fs::create_dir(&external_target).unwrap();
std::fs::write(external_target.join("secret.txt"), b"system secret").unwrap();
let junction_path = td.path().join("malicious_junction");
let output = Command::new("cmd")
.args([
"/C",
"mklink",
"/J",
&junction_path.to_string_lossy(),
&external_target.to_string_lossy(),
])
.output()
.unwrap();
if !output.status.success() {
eprintln!(
"Failed to create junction: {}",
String::from_utf8_lossy(&output.stderr)
);
return;
}
let vpath = vroot
.virtual_join("malicious_junction/secret.txt")
.expect("Junction should be resolved with clamping");
let canonical_vroot = std::fs::canonicalize(td.path()).unwrap();
assert!(
vpath.as_unvirtual().strictpath_starts_with(&canonical_vroot),
"Junction target MUST be clamped within virtual root.\nGot: {:?}\nVRoot: {canonical_vroot:?}\nOriginal target: {external_target:?}",
vpath.as_unvirtual().strictpath_display()
);
let read_result = vpath.read_to_string();
assert!(
read_result.is_err(),
"Reading clamped junction should fail (file doesn't exist at clamped location)"
);
}
#[test]
#[cfg(all(windows, feature = "virtual-path"))]
fn following_junction_with_relative_escape() {
use std::process::Command;
let td = tempfile::tempdir().unwrap();
let vroot: VirtualRoot = VirtualRoot::try_new_create(td.path()).unwrap();
let nested_dir = td.path().join("user").join("uploads");
std::fs::create_dir_all(&nested_dir).unwrap();
let system_td = tempfile::tempdir().unwrap();
let external_target = system_td.path().join("external_data");
std::fs::create_dir(&external_target).unwrap();
std::fs::write(external_target.join("data.txt"), b"external data").unwrap();
let junction_path = nested_dir.join("escape_link");
let output = Command::new("cmd")
.args([
"/C",
"mklink",
"/J",
&junction_path.to_string_lossy(),
&external_target.to_string_lossy(),
])
.output()
.unwrap();
if !output.status.success() {
eprintln!(
"Failed to create junction: {}",
String::from_utf8_lossy(&output.stderr)
);
return;
}
let vpath = vroot
.virtual_join("user/uploads/escape_link/data.txt")
.expect("Junction should be resolved with clamping");
let canonical_vroot = std::fs::canonicalize(td.path()).unwrap();
assert!(
vpath.as_unvirtual().strictpath_starts_with(&canonical_vroot),
"Junction target MUST be clamped within virtual root.\nGot: {:?}\nVRoot: {canonical_vroot:?}\nOriginal target: {external_target:?}",
vpath.as_unvirtual().strictpath_display()
);
let read_result = vpath.read_to_string();
assert!(
read_result.is_err(),
"Reading clamped junction should fail (file doesn't exist at clamped location)"
);
}