feat(security): release v0.7.0 with comprehensive security hardening (S-01 to S-11)
Sanctum Release / Build & Release (Windows x86_64) (push) Waiting to run

This commit is contained in:
2026-09-18 19:12:43 +02:00
parent 80a3bd1911
commit 436790abf0
29 changed files with 1553 additions and 277 deletions
+76 -14
View File
@@ -209,13 +209,27 @@ pub async fn mount_container(
ui::step(3, 4, "🔓", "Notfallschlüssel erfolgreich verifiziert!");
}
// Prüfe, ob dek Dateien im Hidden Vault (Root 2) entschlüsseln kann
let is_hidden = {
let children = db.list_children_in_vault(2, 1, &dek).unwrap_or_default();
!children.is_empty()
let carrier_node_id = db.find_carrier_node_id()?.unwrap_or(0);
let (carrier_dek, final_carrier_node_id, vault_id) = if carrier_node_id > 0 {
let slot_id = crate::recovery::detect_recovery_key_slot(&db, &dek, meta.version)?;
if slot_id == 1 {
if !stealth {
println!(" [i] Der angegebene Notfallschlüssel gehört zum Hidden-Vault (Slot 1).");
println!(" Für den Zugriff auf die Trägerdatei wird das Passwort des Standard-Vaults benötigt.");
}
let decoy_pass = rpassword::prompt_password("Master-Passwort für Standard-Vault eingeben: ")
.context("Fehler beim Einlesen des Standard-Vault Passworts")?;
let decoy_keys = meta.authenticate(&decoy_pass)
.ok_or_else(|| anyhow::anyhow!("Ungültiges Passwort für Standard-Vault."))?;
(Some(decoy_keys.dek().clone()), Some(carrier_node_id), 1)
} else {
(None, Some(carrier_node_id), 0)
}
} else {
(None, None, 0)
};
let vault_id = if is_hidden { 1 } else { 0 };
(dek, None, None, meta.version, vault_id)
(dek, carrier_dek, final_carrier_node_id, meta.version, vault_id)
}
};
@@ -402,12 +416,12 @@ pub async fn mount_container(
println!(" • Modus: WebDAV Userland-VFS");
}
}
println!(" • WebDAV-URL: http://127.0.0.1:{}/{}/ (Session-Token geschützt)", bound_port, session_token);
println!(" • WebDAV-URL: http://127.0.0.1:{}/<session-token>/ (lokal geschützt)", bound_port);
#[cfg(not(windows))]
{
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/{}/", bound_port, session_token);
println!(" • gio Befehl: gio mount dav://127.0.0.1:{}/<session-token>/", bound_port);
if let Some(mp) = mount_point {
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/{}/ {}", bound_port, session_token, mp.display());
println!(" • davfs2: mount -t davfs http://127.0.0.1:{}/<session-token>/ {}", bound_port, mp.display());
}
}
if let Some(secs) = idle_timeout {
@@ -602,15 +616,63 @@ pub async fn mount_container(
println!("{} Sanctum Container wurde sicher und vollständig geschlossen.", ui::green(""));
println!();
}
drop(db);
// S-10: Verifikation und Bereinigung von temporären SQLite WAL- und SHM-Dateien
let base_os = container_path.as_os_str().to_os_string();
let mut wal = base_os.clone();
wal.push("-wal");
let _ = std::fs::remove_file(wal);
let mut shm = base_os;
shm.push("-shm");
let _ = std::fs::remove_file(shm);
Ok(())
}
/// Überprüft, ob ein Host-Header auf Loopback (127.0.0.1, localhost, [::1]) zeigt (Anti-DNS-Rebinding).
/// Überprüft, ob ein Host-Header exakt auf Loopback (127.0.0.1, localhost, [::1], ::1) zeigt (Anti-DNS-Rebinding & S-06).
/// Striktes Parsing von Host und Port ohne anfällige starts_with-Präfixprüfungen.
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]")
let host_trimmed = host_str.trim().to_ascii_lowercase();
if host_trimmed.is_empty() {
return false;
}
if host_trimmed == "::1" {
return true;
}
let host_part = if host_trimmed.starts_with('[') {
// IPv6 bracketed: [::1] oder [::1]:port
if let Some(bracket_end) = host_trimmed.find(']') {
let inside = &host_trimmed[1..bracket_end];
let after = &host_trimmed[bracket_end + 1..];
if !after.is_empty() {
if !after.starts_with(':') {
return false;
}
if after[1..].parse::<u16>().is_err() {
return false;
}
}
inside
} else {
return false;
}
} else {
// IPv4 oder Hostname: localhost, localhost:8443, 127.0.0.1, 127.0.0.1:8443
if let Some((h, p)) = host_trimmed.rsplit_once(':') {
if p.parse::<u16>().is_ok() {
h
} else {
return false;
}
} else {
&host_trimmed
}
};
matches!(host_part, "127.0.0.1" | "localhost" | "::1")
}
/// Maximale Anzahl gleichzeitiger Verbindungen zum lokalen WebDAV-Endpunkt (Schutz gegen Socket-Exhaustion).