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