ullun-api 0.3.2

Express.js-inspired web framework for Rust - simple, fast, batteries-included
Documentation
//! Penetration testing - security attack simulations

use ullun::prelude::*;

#[test]
#[should_panic(expected = "Invalid cookie name")]
fn test_cookie_injection_name() {
    // Attempt cookie header injection via name
    let _cookie = Cookie::new("session;admin=true", "value");
}

#[test]
#[should_panic(expected = "Invalid cookie value")]
fn test_cookie_injection_value() {
    // Attempt cookie header injection via value
    let _cookie = Cookie::new("session", "abc;HttpOnly=false");
}

#[test]
#[should_panic(expected = "Invalid cookie name")]
fn test_cookie_control_chars_name() {
    // Attempt control character injection
    let _cookie = Cookie::new("session\r\nSet-Cookie: admin=true", "value");
}

#[test]
#[should_panic(expected = "Invalid cookie value")]
fn test_cookie_control_chars_value() {
    // Attempt newline injection
    let _cookie = Cookie::new("session", "value\nHTTP/1.1 200 OK");
}

#[test]
fn test_cookie_samesite_none_forces_secure() {
    // SameSite=None must set Secure flag
    let cookie = Cookie::new("session", "value").same_site(ullun::cookies::SameSite::None);

    let header = cookie.to_header_value();
    assert!(header.contains("Secure"));
    assert!(header.contains("SameSite=None"));
}

#[tokio::test]
async fn test_path_traversal_attacks() {
    use hyper::Request;
    use hyper_util::rt::TokioIo;
    use std::fs;
    use tokio::net::TcpStream;

    // Setup: create test directory
    fs::create_dir_all("test_static").ok();
    fs::write("test_static/public.txt", "public").ok();
    fs::create_dir_all("test_static/private").ok();
    fs::write("test_static/private/secret.txt", "secret").ok();

    let server = tokio::spawn(async {
        App::new()
            .serve_static("/files/", "test_static")
            .run("127.0.0.1:3101")
            .await
    });

    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    let stream = TcpStream::connect("127.0.0.1:3101").await.unwrap();
    let io = TokioIo::new(stream);
    let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();

    tokio::task::spawn(async move {
        let _ = conn.await;
    });

    // Attack 1: Path traversal with ../
    let req = Request::builder()
        .uri("http://127.0.0.1:3101/files/../Cargo.toml")
        .body(String::new())
        .unwrap();
    let response = sender.send_request(req).await.unwrap();
    assert_eq!(response.status(), 404, "Should block ../ traversal");

    // Attack 2: Encoded path traversal
    let req = Request::builder()
        .uri("http://127.0.0.1:3101/files/..%2F..%2FCargo.toml")
        .body(String::new())
        .unwrap();
    let response = sender.send_request(req).await.unwrap();
    assert_eq!(response.status(), 404, "Should block encoded traversal");

    // Attack 3: Absolute path injection
    let req = Request::builder()
        .uri("http://127.0.0.1:3101/files//etc/passwd")
        .body(String::new())
        .unwrap();
    let response = sender.send_request(req).await.unwrap();
    assert_eq!(response.status(), 404, "Should block absolute paths");

    server.abort();
    fs::remove_dir_all("test_static").ok();
}

#[tokio::test]
async fn test_body_size_limit_enforcement() {
    use http_body_util::BodyExt;
    use hyper::Request;
    use hyper_util::rt::TokioIo;
    use tokio::net::TcpStream;

    let server = tokio::spawn(async {
        App::new()
            .max_body_size(Some(100)) // 100 bytes max
            .post("/upload", |_req: ullun::request::Request| async {
                Ok(Response::text("OK"))
            })
            .run("127.0.0.1:3102")
            .await
    });

    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    let stream = TcpStream::connect("127.0.0.1:3102").await.unwrap();
    let io = TokioIo::new(stream);
    let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();

    tokio::task::spawn(async move {
        let _ = conn.await;
    });

    // Attack: Send oversized body
    let large_body = "x".repeat(200);
    let req = Request::builder()
        .method("POST")
        .uri("http://127.0.0.1:3102/upload")
        .header("content-length", large_body.len().to_string())
        .body(large_body)
        .unwrap();

    let response = sender.send_request(req).await.unwrap();
    assert_eq!(response.status(), 413, "Should reject oversized body");

    let body_bytes = response.into_body().collect().await.unwrap().to_bytes();
    let body_str = String::from_utf8_lossy(&body_bytes);
    assert!(
        body_str.contains("too large"),
        "Error message should explain issue"
    );

    server.abort();
}

#[tokio::test]
async fn test_header_injection_protection() {
    use hyper::Request;
    use hyper_util::rt::TokioIo;
    use tokio::net::TcpStream;

    let server = tokio::spawn(async {
        App::new()
            .get("/echo", |_req: ullun::request::Request| async {
                let cookie = Cookie::new("test", "value");
                Ok(Response::text("OK").cookie(cookie))
            })
            .run("127.0.0.1:3103")
            .await
    });

    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    let stream = TcpStream::connect("127.0.0.1:3103").await.unwrap();
    let io = TokioIo::new(stream);
    let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();

    tokio::task::spawn(async move {
        let _ = conn.await;
    });

    let req = Request::builder()
        .uri("http://127.0.0.1:3103/echo")
        .body(String::new())
        .unwrap();

    let response = sender.send_request(req).await.unwrap();

    // Verify Set-Cookie header is well-formed
    let set_cookie = response
        .headers()
        .get("set-cookie")
        .unwrap()
        .to_str()
        .unwrap();
    assert!(!set_cookie.contains("\r\n"), "Should not contain CRLF");
    assert!(!set_cookie.contains("\n"), "Should not contain LF");
    assert_eq!(
        set_cookie.matches("Set-Cookie").count(),
        0,
        "Should not duplicate header"
    );

    server.abort();
}

#[test]
fn test_query_parameter_safety() {
    // Test that query parsing handles malicious input safely
    let query = Query::parse_query_string("<script>alert('xss')</script>=value&key=<img src=x>");

    // Values should be preserved as-is (not decoded/executed)
    assert!(query.get("<script>alert('xss')</script>").is_some());
    assert!(query.get("key").is_some());
    // Framework doesn't auto-escape - that's application's job
}

#[test]
fn test_error_message_safety() {
    // Error messages should not leak sensitive info
    let err =
        Error::internal("Database connection failed: postgres://admin:secret123@localhost/db");

    // In production, this should be sanitized, but for now verify it exists
    assert_eq!(err.status_code(), 500);
    // Note: Actual message sanitization should be done at application level
}

#[test]
fn test_cookie_parsing_malformed() {
    // Test handling of malformed cookie headers
    let _cookies = Cookies::parse(";;;===");
    // Parser will create entries for empty strings, which is acceptable

    let cookies = Cookies::parse("a=b; =noname; novalue=");
    assert!(cookies.get("a").is_some());
    assert_eq!(cookies.get("a"), Some("b"));
    // Empty names are skipped, novalue= creates entry with empty value
    assert!(cookies.get("novalue").is_some());
}

#[tokio::test]
async fn test_concurrent_request_safety() {
    use hyper::Request;
    use hyper_util::rt::TokioIo;
    use tokio::net::TcpStream;

    let server = tokio::spawn(async {
        App::new()
            .get("/slow", |_req: ullun::request::Request| async {
                tokio::time::sleep(tokio::time::Duration::from_millis(50)).await;
                Ok(Response::text("OK"))
            })
            .run("127.0.0.1:3104")
            .await
    });

    tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;

    // Spawn multiple concurrent requests
    let mut handles = vec![];
    for _ in 0..10 {
        let handle = tokio::spawn(async {
            let stream = TcpStream::connect("127.0.0.1:3104").await.unwrap();
            let io = TokioIo::new(stream);
            let (mut sender, conn) = hyper::client::conn::http1::handshake(io).await.unwrap();

            tokio::task::spawn(async move {
                let _ = conn.await;
            });

            let req = Request::builder()
                .uri("http://127.0.0.1:3104/slow")
                .body(String::new())
                .unwrap();

            sender.send_request(req).await.unwrap()
        });
        handles.push(handle);
    }

    // All should complete successfully
    for handle in handles {
        let response = handle.await.unwrap();
        assert_eq!(response.status(), 200);
    }

    server.abort();
}