context-creator 1.5.0

High-performance CLI tool to convert codebases to Markdown for LLM context
Documentation
#![cfg(test)]

//! Tests demonstrating security vulnerabilities before fixes
//! These tests SHOULD FAIL initially, proving the vulnerabilities exist

use context_creator::core::semantic::path_validator::validate_import_path;
use std::fs;
use std::path::Path;
use std::thread;
use std::time::Duration;
use tempfile::TempDir;

#[test]
#[ignore] // Remove ignore after implementing fixes
fn test_toctou_race_condition_vulnerability() {
    // This test demonstrates a Time-of-Check-Time-of-Use vulnerability
    // where an attacker can create a symlink between the check and use

    let temp_dir = TempDir::new().unwrap();
    let base_dir = temp_dir.path();

    // Create a safe target file
    let safe_file = base_dir.join("safe.txt");
    fs::write(&safe_file, "safe content").unwrap();

    // Path that will be swapped via symlink
    let target_path = base_dir.join("target.txt");

    // Initially create a safe file
    fs::write(&target_path, "initial safe content").unwrap();

    // Spawn attacker thread that will swap the file with a symlink
    let attacker_target = target_path.clone();
    let attacker_thread = thread::spawn(move || {
        // Wait a tiny bit for the validation to start
        thread::sleep(Duration::from_micros(100));

        // Remove the safe file and create symlink to /etc/passwd
        fs::remove_file(&attacker_target).ok();
        #[cfg(unix)]
        {
            use std::os::unix::fs::symlink;
            symlink("/etc/passwd", &attacker_target).ok();
        }
    });

    // Try to validate the path multiple times to increase chance of hitting race
    for _ in 0..100 {
        let result = validate_import_path(base_dir, &target_path);

        // If validation passes but file is now a symlink to /etc/passwd, we have a vulnerability
        if result.is_ok() {
            #[cfg(unix)]
            {
                if let Ok(link_target) = fs::read_link(&target_path) {
                    if link_target.to_str() == Some("/etc/passwd") {
                        panic!("VULNERABILITY: Path validation passed for symlink to /etc/passwd!");
                    }
                }
            }
        }
    }

    attacker_thread.join().unwrap();
}

#[test]
fn test_url_encoding_bypass_vulnerability() {
    // This test demonstrates that URL-encoded path traversal can bypass validation

    let temp_dir = TempDir::new().unwrap();
    let base_dir = temp_dir.path();

    // Create a test file structure
    fs::create_dir_all(base_dir.join("src")).unwrap();
    fs::write(base_dir.join("src/main.rs"), "fn main() {}").unwrap();

    // Various URL-encoded path traversal attempts that should be blocked
    let url_encoded_attacks = vec![
        // Single encoding
        "src/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
        "src/%2E%2E/%2E%2E/%2E%2E/etc/passwd", // Uppercase
        "%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
        // Double encoding
        "src/%252e%252e/%252e%252e/etc/passwd",
        // Mixed encoding
        "src/%2e%2e/../etc/passwd",
        "src/../%2e%2e/etc/passwd",
        // Unicode/UTF-8 encoding variants
        "src/%c0%ae%c0%ae/%c0%ae%c0%ae/etc/passwd", // Overlong UTF-8
        // Null byte injection
        "src/../etc/passwd%00.rs",
        // Alternative encodings
        "src/%252E%252E%252F%252E%252E%252Fetc%252Fpasswd",
        // More sophisticated attacks
        "%5c%2e%2e%5c%2e%2e%5cetc%5cpasswd", // Backslash encoding
        "src/%u002e%u002e/%u002e%u002e/etc/passwd", // Unicode encoding
        "src/..%c0%af..%c0%afetc%c0%afpasswd", // Modified UTF-8
        "%2e%2e%252f%2e%2e%252fetc%252fpasswd", // Mixed double encoding
    ];

    for attack in url_encoded_attacks {
        let attack_path = base_dir.join(attack);
        let result = validate_import_path(base_dir, &attack_path);

        if result.is_ok() {
            panic!("VULNERABILITY: URL-encoded attack passed validation: {attack}");
        }
    }
}

#[test]
fn test_mixed_slash_bypass_vulnerability() {
    // Test Windows-style backslash attacks on Unix systems

    let temp_dir = TempDir::new().unwrap();
    let base_dir = temp_dir.path();

    let attacks = vec![
        "src\\..\\..\\etc\\passwd",
        "src/..\\../etc/passwd",
        "src\\../..\\etc/passwd",
        // URL encoded backslashes
        "src%5c..%5c..%5cetc%5cpasswd",
        "src%5C..%5C..%5Cetc%5Cpasswd",
    ];

    for attack in attacks {
        let attack_path = base_dir.join(attack);
        let result = validate_import_path(base_dir, &attack_path);

        // On Unix, backslashes might be treated as literal characters
        // But the validator should normalize these
        if result.is_ok() {
            if let Ok(canonical) = attack_path.canonicalize() {
                if !canonical.starts_with(base_dir) {
                    panic!("VULNERABILITY: Mixed slash attack escaped base directory: {attack}");
                }
            }
        }
    }
}

#[test]
fn test_symlink_escape_vulnerability() {
    // Test if symlinks can escape the project directory

    #[cfg(unix)]
    {
        let temp_dir = TempDir::new().unwrap();
        let base_dir = temp_dir.path();
        use std::os::unix::fs::symlink;

        // Create a symlink pointing outside
        let symlink_path = base_dir.join("escape_link");
        symlink("/etc/passwd", &symlink_path).unwrap();

        let result = validate_import_path(base_dir, &symlink_path);

        if result.is_ok() {
            panic!("VULNERABILITY: Symlink to /etc/passwd passed validation!");
        }
    }
}

#[test]
fn test_absolute_path_outside_project() {
    // Test that absolute paths outside project are rejected

    let temp_dir = TempDir::new().unwrap();
    let base_dir = temp_dir.path();

    let attacks = vec![
        Path::new("/etc/passwd"),
        Path::new("/home/user/.ssh/id_rsa"),
        Path::new("/var/log/secure"),
        #[cfg(windows)]
        Path::new("C:\\Windows\\System32\\config\\SAM"),
    ];

    for attack_path in attacks {
        if attack_path.exists() {
            // Only test if the path exists on this system
            let result = validate_import_path(base_dir, attack_path);

            if result.is_ok() {
                panic!(
                    "VULNERABILITY: Absolute path outside project passed validation: {attack_path:?}"
                );
            }
        }
    }
}

#[test]
fn test_path_normalization_attacks() {
    // Test various path normalization attacks

    let temp_dir = TempDir::new().unwrap();
    let base_dir = temp_dir.path();

    let attacks = vec![
        // Multiple separators
        "src//..//..//..//etc//passwd",
        "src/./././../../../etc/passwd",
        // Current directory references
        "./../../etc/passwd",
        "././././../../../etc/passwd",
        // Trailing dots (Windows)
        "src../../../etc/passwd...",
        "src.../..../.../etc/passwd",
        // Leading dots
        "...src/../../etc/passwd",
    ];

    for attack in attacks {
        let attack_path = base_dir.join(attack);
        let result = validate_import_path(base_dir, &attack_path);

        // Try to resolve manually to check if it escapes
        let mut current = base_dir.to_path_buf();
        for component in Path::new(attack).components() {
            match component {
                std::path::Component::ParentDir => {
                    current.pop();
                }
                std::path::Component::Normal(c) => {
                    current.push(c);
                }
                _ => {}
            }
        }

        if result.is_ok() && !current.starts_with(base_dir) {
            panic!("VULNERABILITY: Path normalization attack escaped base directory: {attack}");
        }
    }
}