fix(security): implement P1 loopback session token, P2 AAD filename binding, and P4 memory zeroization

This commit is contained in:
2026-09-09 21:24:07 +02:00
parent 98bfac718f
commit 771c08ff0f
7 changed files with 323 additions and 64 deletions
+7 -1
View File
@@ -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())
+92 -13
View File
@@ -189,16 +189,29 @@ pub fn generate_dummy_slot() -> (Vec<u8>, [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<String> {
/// 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<String> {
// Abwärtskompatibilität für alte v0.2.0 $h$<nonce>$<tag>$<ct> 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<String> {
) {
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<String> {
{
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<String> {
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<String> {
{
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$<nonce>$<tag>$<ct> 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();
+97 -43
View File
@@ -55,9 +55,9 @@ pub fn unmount_drive(drive_letter: char) -> Result<()> {
Ok(())
}
/// Bindet ein Windows-Netzlaufwerk via `net use <DRIVE>: http://127.0.0.1:<PORT>/ /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 <DRIVE>: http://127.0.0.1:<PORT>/<TOKEN>/ /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<bool>,
) {
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;
}
}
}
}
+6 -5
View File
@@ -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<i64> = 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()
};
+1 -1
View File
@@ -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;
}
+7 -1
View File
@@ -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())
+113
View File
@@ -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);
}