fix(security): implement P1 loopback session token, P2 AAD filename binding, and P4 memory zeroization
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user