From 771c08ff0f45ac525e09f1575c932d9a39b16752 Mon Sep 17 00:00:00 2001 From: harald Date: Wed, 9 Sep 2026 21:24:07 +0200 Subject: [PATCH] fix(security): implement P1 loopback session token, P2 AAD filename binding, and P4 memory zeroization --- src/carrier.rs | 8 ++- src/crypto.rs | 105 ++++++++++++++++++++++++---- src/mount.rs | 140 ++++++++++++++++++++++++++------------ src/storage.rs | 11 +-- src/verify.rs | 2 +- src/vfs.rs | 8 ++- tests/integration_test.rs | 113 ++++++++++++++++++++++++++++++ 7 files changed, 323 insertions(+), 64 deletions(-) diff --git a/src/carrier.rs b/src/carrier.rs index 2509531..d4e273d 100644 --- a/src/carrier.rs +++ b/src/carrier.rs @@ -19,7 +19,7 @@ use rand::rngs::OsRng; use rand::RngCore; use serde::{Deserialize, Serialize}; use tracing::error; -use zeroize::Zeroizing; +use zeroize::{Zeroize, Zeroizing}; use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE}; use crate::storage::Database; @@ -825,6 +825,9 @@ impl CarrierFile { } self.flush_cached_block()?; + if let Some((_, ref mut data, _)) = self.cached_block { + data.zeroize(); + } let data = if block_idx_in_file < self.blocks.len() { let carrier_block_idx = self.blocks[block_idx_in_file]; @@ -999,6 +1002,9 @@ impl Drop for CarrierFile { if let Err(e) = self.flush_cached_block() { tracing::warn!("Fehler beim automatischen Flush im CarrierFile::drop: {:?}", e); } + if let Some((_, ref mut data, _)) = self.cached_block { + data.zeroize(); + } let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) diff --git a/src/crypto.rs b/src/crypto.rs index 14edc37..f29c68b 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -189,16 +189,29 @@ pub fn generate_dummy_slot() -> (Vec, [u8; 12], [u8; 16], [u8; 16]) { (wrapped_dek, nonce, tag, salt) } -/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM. -/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind. +/// Erzeugt die 16-Byte Associated Data (AAD) für einen Dateinamen im Hidden Vault, +/// um Directory-Hijacking und Cross-Node Name-Substitution-Angriffe kryptografisch zu verhindern: +/// Magic "SANCNAME" (8 Bytes) || parent_id (8 Bytes Little-Endian). +#[inline] +pub fn build_name_aad(parent_id: i64) -> [u8; 16] { + let mut aad = [0u8; 16]; + aad[..8].copy_from_slice(b"SANCNAME"); + aad[8..].copy_from_slice(&parent_id.to_le_bytes()); + aad +} + +/// Verschlüsselt den Dateinamen für Knoten im Hidden Vault mit AES-256-GCM und bindet die parent_id als AAD ein. +/// Verhindert, dass unverschlüsselte Dateinamen in der SQLite-Datenbank forensisch auffindbar sind +/// und verhindert, dass verschlüsselte Knoten zwischen Ordnern verschoben oder vertauscht werden können. /// Verwendet reines Hex-Encoding ohne verräterisches Präfix (12B Nonce + 16B Tag + Ciphertext). -pub fn encrypt_node_name(dek: &[u8; 32], name: &str) -> String { +pub fn encrypt_node_name(dek: &[u8; 32], parent_id: i64, name: &str) -> String { let mut nonce_bytes = [0u8; 12]; OsRng.fill_bytes(&mut nonce_bytes); let cipher = Aes256Gcm::new_from_slice(dek).expect("AES init"); let mut buffer = name.as_bytes().to_vec(); + let aad = build_name_aad(parent_id); let tag = cipher - .encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), b"SANCTUM_NODE_NAME", &mut buffer) + .encrypt_in_place_detached(Nonce::from_slice(&nonce_bytes), &aad, &mut buffer) .expect("Name encryption"); let mut combined = Vec::with_capacity(12 + 16 + buffer.len()); combined.extend_from_slice(&nonce_bytes); @@ -208,7 +221,9 @@ pub fn encrypt_node_name(dek: &[u8; 32], name: &str) -> String { } /// Entschlüsselt den Dateinamen eines Knotens im Hidden Vault mit AES-256-GCM. -pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option { +/// Prüft primär die kryptografische Bindung an parent_id; bietet transparenten Fallback +/// auf die statische AAD für ältere Container (Abwärtskompatibilität). +pub fn decrypt_node_name(dek: &[u8; 32], parent_id: i64, stored: &str) -> Option { // Abwärtskompatibilität für alte v0.2.0 $h$$$ Namen if let Some(rest) = stored.strip_prefix("$h$") { let parts: Vec<&str> = rest.split('$').collect(); @@ -220,11 +235,13 @@ pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option { ) { if nonce_bytes.len() == 12 && tag_bytes.len() == 16 { let cipher = Aes256Gcm::new_from_slice(dek).ok()?; - let mut buffer = ct_bytes; + // 1. Primär: Authentifizierung mit parent_id AAD + let aad = build_name_aad(parent_id); + let mut buffer = ct_bytes.clone(); if cipher .decrypt_in_place_detached( Nonce::from_slice(&nonce_bytes), - b"SANCTUM_NODE_NAME", + &aad, &mut buffer, Tag::from_slice(&tag_bytes), ) @@ -232,13 +249,27 @@ pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option { { return String::from_utf8(buffer).ok(); } + + // 2. Fallback: Statische AAD für echte Legacy-Dateinamen + let mut buffer_legacy = ct_bytes; + if cipher + .decrypt_in_place_detached( + Nonce::from_slice(&nonce_bytes), + b"SANCTUM_NODE_NAME", + &mut buffer_legacy, + Tag::from_slice(&tag_bytes), + ) + .is_ok() + { + return String::from_utf8(buffer_legacy).ok(); + } } } } return None; } - // Neuer Standard: Reiner Hex-String ohne Präfix (12B Nonce + 16B Tag + Ciphertext) + // Reiner Hex-String (12B Nonce + 16B Tag + Ciphertext) if stored.len() >= 56 { if let Ok(bytes) = hex::decode(stored) { if bytes.len() >= 28 { @@ -247,11 +278,13 @@ pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option { let ct = &bytes[28..]; if let Ok(cipher) = Aes256Gcm::new_from_slice(dek) { + // 1. Primär: Authentifizierung mit parent_id AAD + let aad = build_name_aad(parent_id); let mut buffer = ct.to_vec(); if cipher .decrypt_in_place_detached( Nonce::from_slice(nonce), - b"SANCTUM_NODE_NAME", + &aad, &mut buffer, Tag::from_slice(tag), ) @@ -259,6 +292,20 @@ pub fn decrypt_node_name(dek: &[u8; 32], stored: &str) -> Option { { return String::from_utf8(buffer).ok(); } + + // 2. Fallback: Alte statische AAD für bestehende Container + let mut buffer_legacy = ct.to_vec(); + if cipher + .decrypt_in_place_detached( + Nonce::from_slice(nonce), + b"SANCTUM_NODE_NAME", + &mut buffer_legacy, + Tag::from_slice(tag), + ) + .is_ok() + { + return String::from_utf8(buffer_legacy).ok(); + } } } } @@ -570,23 +617,40 @@ mod tests { fn test_hidden_node_name_encryption_and_dummy_slot() { let dek = generate_dek(); let filename = "ultra_geheimes_dokument.pdf"; - let encrypted = encrypt_node_name(&dek, filename); + let parent_id = 2i64; + let encrypted = encrypt_node_name(&dek, parent_id, filename); // Kein verräterisches Präfix mehr! Reines Hex. assert!(!encrypted.starts_with("$h$")); assert!(!encrypted.contains(filename)); assert!(encrypted.len() >= 56); - let decrypted = decrypt_node_name(&dek, &encrypted).expect("Decrypt name"); + let decrypted = decrypt_node_name(&dek, parent_id, &encrypted).expect("Decrypt name"); assert_eq!(decrypted, filename); // Abwärtskompatibilität: Legacy $h$$$ Format muss weiter entschlüsselt werden let legacy_format = format!("$h${}${}${}", &encrypted[0..24], &encrypted[24..56], &encrypted[56..]); - let decrypted_legacy = decrypt_node_name(&dek, &legacy_format).expect("Decrypt legacy $h$ name"); + let decrypted_legacy = decrypt_node_name(&dek, parent_id, &legacy_format).expect("Decrypt legacy $h$ name"); assert_eq!(decrypted_legacy, filename); + // Echte statische AAD Legacy-Verschlüsselung (b"SANCTUM_NODE_NAME") + let cipher = Aes256Gcm::new_from_slice(&dek[..]).unwrap(); + let mut static_buf = filename.as_bytes().to_vec(); + let static_nonce = [42u8; 12]; + let static_tag = cipher + .encrypt_in_place_detached(Nonce::from_slice(&static_nonce), b"SANCTUM_NODE_NAME", &mut static_buf) + .unwrap(); + let legacy_static_format = format!( + "$h${}${}${}", + hex::encode(static_nonce), + hex::encode(static_tag), + hex::encode(&static_buf) + ); + let decrypted_static = decrypt_node_name(&dek, parent_id, &legacy_static_format).expect("Decrypt legacy static AAD name"); + assert_eq!(decrypted_static, filename); + // Mit anderem DEK schlägt Entschlüsselung fehl let other_dek = generate_dek(); - assert!(decrypt_node_name(&other_dek, &encrypted).is_none()); + assert!(decrypt_node_name(&other_dek, parent_id, &encrypted).is_none()); // Dummy-Slot hat korrekte Längen (72 Bytes für Modell A) let (dummy_dek, dummy_nonce, dummy_tag, dummy_salt) = generate_dummy_slot(); @@ -596,6 +660,21 @@ mod tests { assert_eq!(dummy_salt.len(), 16); } + #[test] + fn test_node_name_aad_parent_binding() { + let dek = generate_dek(); + let enc_folder_a = encrypt_node_name(&dek, 10, "secrets.txt"); + let enc_folder_b = encrypt_node_name(&dek, 20, "passwords.txt"); + + // Gültige parent_ids entschlüsseln erfolgreich + assert_eq!(decrypt_node_name(&dek, 10, &enc_folder_a).unwrap(), "secrets.txt"); + assert_eq!(decrypt_node_name(&dek, 20, &enc_folder_b).unwrap(), "passwords.txt"); + + // Swap-Angriff: Ein Angreifer verschiebt enc_folder_a in Ordner 20 + assert!(decrypt_node_name(&dek, 20, &enc_folder_a).is_none(), "Swap in anderen Ordner muss durch AAD fehlschlagen!"); + assert!(decrypt_node_name(&dek, 10, &enc_folder_b).is_none(), "Swap in anderen Ordner muss durch AAD fehlschlagen!"); + } + #[test] fn test_model_a_slot_payloads() { let kek_0 = derive_kek("DecoyPass123!", &generate_salt(), &KdfParams { memory_cost: 1024, time_cost: 1, parallelism: 1 }).unwrap(); diff --git a/src/mount.rs b/src/mount.rs index f565793..912a5d1 100644 --- a/src/mount.rs +++ b/src/mount.rs @@ -55,9 +55,9 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> { Ok(()) } -/// Bindet ein Windows-Netzlaufwerk via `net use : http://127.0.0.1:/ /persistent:no` ein. -fn run_net_use_mount(drive_str: &str, port: u16) -> Result<()> { - let url = format!("http://127.0.0.1:{}/", port); +/// Bindet ein Windows-Netzlaufwerk via `net use : http://127.0.0.1:// /persistent:no` ein. +fn run_net_use_mount(drive_str: &str, port: u16, session_token: &str) -> Result<()> { + let url = format!("http://127.0.0.1:{}/{}/", port, session_token); let output = Command::new("net") .args(["use", drive_str, &url, "/persistent:no"]) @@ -147,6 +147,12 @@ pub async fn mount_container( } }; + // 128-Bit Session-Token für Loopback-Schutz (CWE-306) & Anti-CSRF generieren + let mut token_bytes = [0u8; 16]; + rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut token_bytes); + let session_token = hex::encode(token_bytes); + let token_path_prefix = format!("/{}", session_token); + // WebDAV Filesystem und Handler konfigurieren (mit Anti-Leak Shield & Carrier-Routing) let fs = SanctumFs::with_carrier( db.clone(), @@ -159,6 +165,7 @@ pub async fn mount_container( ); let last_activity = fs.last_activity(); let dav_server = DavHandler::builder() + .strip_prefix(token_path_prefix.clone()) .filesystem(Box::new(fs)) .locksystem(FakeLs::new()) .build_handler(); @@ -190,49 +197,18 @@ pub async fn mount_container( ), ); - let (shutdown_tx, mut shutdown_rx) = watch::channel(false); + let (shutdown_tx, shutdown_rx) = watch::channel(false); // Hyper HTTP Server Loop im Hintergrund starten - let server_dav = dav_server.clone(); - let server_handle = tokio::spawn(async move { - loop { - tokio::select! { - res = listener.accept() => { - let (stream, _) = match res { - Ok(val) => val, - Err(e) => { - warn!("Verbindungsfehler im TCP-Listener: {e}"); - continue; - } - }; - - let io = TokioIo::new(stream); - let handler = server_dav.clone(); - - tokio::spawn(async move { - let service = service_fn(move |req| { - let h = handler.clone(); - async move { - Ok::<_, Infallible>(h.handle(req).await) - } - }); - - if let Err(err) = http1::Builder::new().serve_connection(io, service).await { - // Client-Disconnects im Explorer sind normal - debug!("HTTP-Verbindungsende: {:?}", err); - } - }); - } - _ = shutdown_rx.changed() => { - debug!("WebDAV-Server-Task empfängt Shutdown-Signal."); - break; - } - } - } - }); + let server_handle = tokio::spawn(serve_webdav_loop( + listener, + dav_server, + token_path_prefix.clone(), + shutdown_rx, + )); // Netzlaufwerk einbinden - if let Err(e) = run_net_use_mount(&drive_str, bound_port) { + if let Err(e) = run_net_use_mount(&drive_str, bound_port, &session_token) { let _ = shutdown_tx.send(true); let _ = server_handle.await; return Err(e); @@ -322,7 +298,7 @@ pub async fn mount_container( println!(); println!(" • Container: {}", container_path.display()); println!(" • Netzlaufwerk: {} (im Windows Explorer bereit)", ui::cyan(&drive_str)); - println!(" • WebDAV-URL: http://127.0.0.1:{}/", bound_port); + println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token); if vault_id == 1 { println!(" • Vault: {} (Verschlüsselte Dateinamen)", ui::magenta("Hidden Vault (Slot 1)")); } @@ -403,3 +379,81 @@ pub async fn mount_container( Ok(()) } +/// Überprüft, ob ein Host-Header auf Loopback (127.0.0.1, localhost, [::1]) zeigt (Anti-DNS-Rebinding). +pub fn is_loopback_host(host_str: &str) -> bool { + let host_lower = host_str.to_ascii_lowercase(); + host_lower.starts_with("127.0.0.1") + || host_lower.starts_with("localhost") + || host_lower.starts_with("[::1]") +} + +/// Führt die asynchrone WebDAV HTTP-Server-Schleife mit Session-Token & Host-Header Sicherheits-Middleware aus. +pub async fn serve_webdav_loop( + listener: TcpListener, + dav_server: DavHandler, + token_path_prefix: String, + mut shutdown_rx: watch::Receiver, +) { + loop { + tokio::select! { + res = listener.accept() => { + let (stream, _) = match res { + Ok(val) => val, + Err(e) => { + warn!("Verbindungsfehler im TCP-Listener: {e}"); + continue; + } + }; + + let io = TokioIo::new(stream); + let handler = dav_server.clone(); + let expected_prefix = token_path_prefix.clone(); + + tokio::spawn(async move { + let service = service_fn(move |req| { + let h = handler.clone(); + let prefix = expected_prefix.clone(); + async move { + // 1. Host-Header Validierung (Anti-DNS-Rebinding) + if let Some(host_val) = req.headers().get(hyper::header::HOST) { + if let Ok(host_str) = host_val.to_str() { + if !is_loopback_host(host_str) { + warn!("Abgewiesener Zugriff mit externem Host-Header: {}", host_str); + let res = hyper::Response::builder() + .status(hyper::StatusCode::FORBIDDEN) + .body(dav_server::body::Body::empty()) + .unwrap(); + return Ok::<_, Infallible>(res); + } + } + } + + // 2. Session-Token Pfadprüfung (Loopback-Schutz gegen unbefugte lokale Prozesse & Browser CSRF) + let path = req.uri().path(); + if !path.starts_with(&prefix) { + debug!("Abgewiesener Zugriff ohne gültiges Session-Token: {}", path); + let res = hyper::Response::builder() + .status(hyper::StatusCode::FORBIDDEN) + .body(dav_server::body::Body::empty()) + .unwrap(); + return Ok::<_, Infallible>(res); + } + + Ok::<_, Infallible>(h.handle(req).await) + } + }); + + if let Err(err) = http1::Builder::new().serve_connection(io, service).await { + // Client-Disconnects im Explorer sind normal + debug!("HTTP-Verbindungsende: {:?}", err); + } + }); + } + _ = shutdown_rx.changed() => { + debug!("WebDAV-Server-Task empfängt Shutdown-Signal."); + break; + } + } + } +} + diff --git a/src/storage.rs b/src/storage.rs index 9c763cc..e03dfea 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -867,7 +867,7 @@ impl Database { let mut matched_record = None; for r in rows { let (id, p_id, enc_name, is_dir, size, c_at, m_at) = r?; - let dec_name = decrypt_node_name(dek, &enc_name).unwrap_or(enc_name); + let dec_name = decrypt_node_name(dek, p_id.unwrap_or(0), &enc_name).unwrap_or(enc_name); if dec_name == *segment { matched_record = Some(NodeRecord { id, @@ -918,8 +918,9 @@ impl Database { let record = stmt .query_row(params![id], |row| { let enc_name: String = row.get(2)?; + let p_id: Option = row.get(1)?; let name = if vault_id == 1 { - decrypt_node_name(dek, &enc_name).unwrap_or(enc_name) + decrypt_node_name(dek, p_id.unwrap_or(0), &enc_name).unwrap_or(enc_name) } else { enc_name }; @@ -961,7 +962,7 @@ impl Database { let rows = stmt.query_map(params![parent_id], |row| { let enc_name: String = row.get(2)?; let name = if vault_id == 1 { - decrypt_node_name(dek, &enc_name).unwrap_or(enc_name) + decrypt_node_name(dek, parent_id, &enc_name).unwrap_or(enc_name) } else { enc_name }; @@ -1002,7 +1003,7 @@ impl Database { let conn = self.conn.lock().unwrap(); let stored_name = if vault_id == 1 { - encrypt_node_name(dek, name) + encrypt_node_name(dek, parent_id, name) } else { name.to_string() }; @@ -1084,7 +1085,7 @@ impl Database { let now = current_timestamp(); let conn = self.conn.lock().unwrap(); let stored_name = if vault_id == 1 { - encrypt_node_name(dek, new_name) + encrypt_node_name(dek, new_parent_id, new_name) } else { new_name.to_string() }; diff --git a/src/verify.rs b/src/verify.rs index 0f522c1..13bb137 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -213,7 +213,7 @@ pub fn verify_container( let mut found_v1 = false; for node in &all_nodes { if node.parent_id == Some(2) { - if crate::crypto::decrypt_node_name(active_dek, &node.name).is_some() { + if crate::crypto::decrypt_node_name(active_dek, 2, &node.name).is_some() { found_v1 = true; break; } diff --git a/src/vfs.rs b/src/vfs.rs index 9bbb05b..dcddd7b 100644 --- a/src/vfs.rs +++ b/src/vfs.rs @@ -14,7 +14,7 @@ use dav_server::{ }; use futures_util::stream; use tracing::{debug, error, warn}; -use zeroize::Zeroizing; +use zeroize::{Zeroize, Zeroizing}; use crate::carrier::CarrierFs; use crate::crypto::{decrypt_chunk, encrypt_chunk, CHUNK_SIZE}; @@ -200,6 +200,9 @@ impl SanctumFile { if !is_current { self.flush_cached_chunk()?; + if let Some((_, ref mut data, _)) = self.cached_chunk { + data.zeroize(); + } let payload = match self.db.read_chunk(self.node_id, chunk_index).map_err(|e| { error!("Fehler beim Lesen des Chunks #{chunk_index}: {e}"); @@ -236,6 +239,9 @@ impl Drop for SanctumFile { if let Err(e) = self.flush_cached_chunk() { warn!("Fehler beim automatischen Flush im SanctumFile::drop: {:?}", e); } + if let Some((_, ref mut data, _)) = self.cached_chunk { + data.zeroize(); + } let now = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 620ad19..756419e 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -1050,6 +1050,119 @@ async fn test_sanctum_online_backup_and_restore() { } } +#[tokio::test] +async fn test_webdav_loopback_session_token_and_host_validation() { + use dav_server::{fakels::FakeLs, DavHandler}; + use sanctum::mount::{is_loopback_host, serve_webdav_loop}; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::{TcpListener, TcpStream}; + // 1. Host-Header Unit Validierung + assert!(is_loopback_host("127.0.0.1")); + assert!(is_loopback_host("127.0.0.1:8443")); + assert!(is_loopback_host("localhost")); + assert!(is_loopback_host("localhost:8443")); + assert!(is_loopback_host("[::1]")); + assert!(is_loopback_host("[::1]:8443")); + assert!(!is_loopback_host("attacker.com")); + assert!(!is_loopback_host("evil.attacker.com:8443")); + assert!(!is_loopback_host("192.168.1.100")); + assert!(!is_loopback_host("internal.corp")); + // 2. Container und WebDAV Setup + let temp_dir = std::env::temp_dir(); + let container_path = temp_dir.join(format!("test_webdav_sec_{}.sanctum", std::process::id())); + if container_path.exists() { + let _ = std::fs::remove_file(&container_path); + } + let password = "WebDavSecPassword2026!"; + let salt = generate_salt(); + let kdf_params = KdfParams { + memory_cost: 1024, + time_cost: 1, + parallelism: 1, + }; + let kek = derive_kek(password, &salt, &kdf_params).unwrap(); + let dek = generate_dek(); + let (wrapped_dek, header_nonce, header_tag) = wrap_dek(&kek, &dek).unwrap(); + + let db = Database::open(&container_path).unwrap(); + db.init_schema(&salt, &kdf_params, &wrapped_dek, &header_nonce, &header_tag).unwrap(); + db.checkpoint().unwrap(); + + let fs = SanctumFs::new(db.clone(), dek.clone(), FORMAT_VERSION); + let session_token = "deadbeefcafebabe0123456789abcdef"; + let token_path_prefix = format!("/{}", session_token); + + let dav_server = DavHandler::builder() + .strip_prefix(token_path_prefix.clone()) + .filesystem(Box::new(fs)) + .locksystem(FakeLs::new()) + .build_handler(); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + let (shutdown_tx, shutdown_rx) = tokio::sync::watch::channel(false); + let server_handle = tokio::spawn(serve_webdav_loop( + listener, + dav_server, + token_path_prefix.clone(), + shutdown_rx, + )); + + // 3. Angriffstest A: Unbefugter Zugriff auf Root ohne Session-Token -> 403 Forbidden + { + let mut stream = TcpStream::connect(addr).await.unwrap(); + let req = format!("GET / HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n", addr.port()); + stream.write_all(req.as_bytes()).await.unwrap(); + let mut resp = Vec::new(); + stream.read_to_end(&mut resp).await.unwrap(); + let resp_str = String::from_utf8_lossy(&resp); + assert!( + resp_str.starts_with("HTTP/1.1 403 Forbidden"), + "Anfrage ohne Session-Token muss mit 403 Forbidden abgewiesen werden, erhalten:\n{}", + resp_str + ); + } + + // 4. Angriffstest B: DNS-Rebinding mit fremdem Host-Header trotz erratenem Token -> 403 Forbidden + { + let mut stream = TcpStream::connect(addr).await.unwrap(); + let req = format!("GET /{}/ HTTP/1.1\r\nHost: evil.attacker.com:{}\r\nConnection: close\r\n\r\n", session_token, addr.port()); + stream.write_all(req.as_bytes()).await.unwrap(); + let mut resp = Vec::new(); + stream.read_to_end(&mut resp).await.unwrap(); + let resp_str = String::from_utf8_lossy(&resp); + assert!( + resp_str.starts_with("HTTP/1.1 403 Forbidden"), + "Anfrage mit fremdem Host-Header (DNS Rebinding) muss mit 403 Forbidden abgewiesen werden, erhalten:\n{}", + resp_str + ); + } + + // 5. Legitimer Zugriff: Korrektes Session-Token & Loopback-Host -> 200 OK + { + let mut stream = TcpStream::connect(addr).await.unwrap(); + let req = format!("OPTIONS /{}/ HTTP/1.1\r\nHost: 127.0.0.1:{}\r\nConnection: close\r\n\r\n", session_token, addr.port()); + stream.write_all(req.as_bytes()).await.unwrap(); + let mut resp = Vec::new(); + stream.read_to_end(&mut resp).await.unwrap(); + let resp_str = String::from_utf8_lossy(&resp); + assert!( + resp_str.starts_with("HTTP/1.1 200 OK"), + "Legitime OPTIONS-Anfrage mit Session-Token muss 200 OK liefern, erhalten:\n{}", + resp_str + ); + assert!( + resp_str.to_lowercase().contains("dav:"), + "Antwort muss WebDAV DAV-Header enthalten" + ); + } + + // Server ordnungsgemäß beenden & aufräumen + let _ = shutdown_tx.send(true); + let _ = server_handle.await; + let _ = std::fs::remove_file(&container_path); +}