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
+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);
}