use std::fs;
#[cfg(feature = "anchored")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(unix)]
use soft_canonicalize::anchored_canonicalize;
println!("\n╔══════════════════════════════════════════════════════════════╗");
println!("║ SCENARIO: Symlinks in User-Uploaded Archives ║");
println!("╚══════════════════════════════════════════════════════════════╝\n");
let temp = std::env::temp_dir().join("archive_demo");
let _ = fs::remove_dir_all(&temp);
fs::create_dir_all(&temp)?;
fs::create_dir_all(temp.join("user_123"))?;
let admin_dir = temp.join("admin");
fs::create_dir_all(&admin_dir)?;
fs::write(admin_dir.join("database.sql"), "DROP TABLE users; --")?;
fs::write(temp.join("secrets.txt"), "API_KEY=secret_12345")?;
println!("🏢 Multi-tenant file server:");
println!(" 📁 user_123/ ← User's isolated directory");
println!(" 📁 admin/database.sql ← Admin files (forbidden!)");
println!(" 📄 secrets.txt ← Server secrets (forbidden!)\n");
#[cfg(unix)]
{
let user_sandbox = temp.join("user_123");
use std::os::unix::fs::symlink;
println!("😈 Attacker uploads malicious.zip:");
let archive = user_sandbox.join("uploaded");
fs::create_dir_all(&archive)?;
fs::write(archive.join("invoice.pdf"), "Looks safe...")?;
symlink("../../secrets.txt", archive.join("config.txt"))?;
symlink("../../admin/database.sql", archive.join("backup.sql"))?;
println!(" 📄 invoice.pdf (legitimate)");
println!(" 🔗 config.txt → ../../secrets.txt");
println!(" 🔗 backup.sql → ../../admin/database.sql\n");
println!("─────────────────────────────────────────────────────────────\n");
println!("📋 APPROACH 1: Reject escaping symlinks (strict semantics):\n");
match std::fs::canonicalize(archive.join("config.txt")) {
Ok(path) => {
if !path.starts_with(&user_sandbox) {
println!(" User reads: 'uploaded/config.txt'");
println!(" System resolves to: {path:?}");
println!(" ⚠️ Detected escape! Reject this file.\n");
println!(" Semantics: System filesystem - symlinks point to");
println!(" their actual targets. Reject escapees.\n");
}
}
Err(e) => println!(" Error: {e}\n"),
}
println!("─────────────────────────────────────────────────────────────\n");
println!("📋 APPROACH 2: Clamp symlinks (virtual filesystem semantics):\n");
match anchored_canonicalize(&user_sandbox, "uploaded/config.txt") {
Ok(safe_path) => {
let relative = safe_path.strip_prefix(&user_sandbox).unwrap();
println!(" User reads: 'uploaded/config.txt'");
println!(" Symlink points to: ../../secrets.txt");
println!(" 🔒 CLAMPED to: user_123/{relative:?}");
println!(" ✅ Symlink is valid within the virtual filesystem.\n");
println!(" Semantics: Virtual filesystem - ALL absolute paths are");
println!(" relative to the anchor. Accept the symlink.\n");
}
Err(e) => println!(" Error: {e}\n"),
}
match anchored_canonicalize(&user_sandbox, "uploaded/backup.sql") {
Ok(safe_path) => {
let relative = safe_path.strip_prefix(&user_sandbox).unwrap();
println!(" User reads: 'uploaded/backup.sql'");
println!(" Symlink points to: ../../admin/database.sql");
println!(" 🔒 CLAMPED to: user_123/{relative:?}");
println!(" ✅ Admin files protected!\n");
}
Err(e) => println!(" Error: {e}\n"),
}
}
#[cfg(windows)]
{
println!("😈 Attacker uploads malicious.zip:");
println!(" 📄 invoice.pdf (legitimate)");
println!(" 🔗 config.txt → C:\\Windows\\System32\\config");
println!(" 🔗 backup.sql → C:\\admin\\database.sql\n");
println!("─────────────────────────────────────────────────────────────");
println!("ℹ️ CONCEPTUAL EXAMPLE (Windows requires admin for symlinks)");
println!(" Run on Linux/macOS to see actual behavior with real symlinks!");
println!("─────────────────────────────────────────────────────────────\n");
println!("📋 APPROACH 1: Strict - reject escaping symlinks");
println!(" → Reject config.txt (escapes sandbox)\n");
println!("📋 APPROACH 2: Virtual - clamp symlinks to anchor");
println!(" → Clamped to user_123\\Windows\\System32\\config");
println!(" → Accept it (treated as relative to virtual root)\n");
}
println!("╔══════════════════════════════════════════════════════════════╗");
println!("║ TWO VALID APPROACHES - Choose Your Semantics ║");
println!("╠══════════════════════════════════════════════════════════════╣");
println!("║ STRICT (reject): System filesystem semantics ║");
println!("║ Symlinks point to actual targets ║");
println!("║ Use: Admin tools, system resources ║");
println!("║ ║");
println!("║ VIRTUAL (clamp): Virtual filesystem semantics (v0.4.0) ║");
println!("║ ALL paths relative to anchor ║");
println!("║ Use: Archives, multi-tenant, sandboxes ║");
println!("╚══════════════════════════════════════════════════════════════╝\n");
let _ = fs::remove_dir_all(&temp);
Ok(())
}
#[cfg(not(feature = "anchored"))]
fn main() {
eprintln!("This example requires the 'anchored' feature.");
eprintln!("Run with: cargo run --example virtual_filesystem_demo --features anchored");
std::process::exit(1);
}