release: v0.7.2 — Security Audit Remediation (SA-01 bis SA-07)
- SA-01: Container-DoS / KDF-Amplification Schutz mit Pre-KDF Validierung, max 2 Slots (nur 0 und 1), Slot 0 Pflicht und strikten BLOB-Laengen - SA-02: Release-Signierung in CI entkoppelt (getrennte build und sign-and-release Jobs, Secret-Isolation) - SA-03: Pinned Download-Integritaet fuer minisign.exe in CI via SHA-256 - SA-04: Immutable Action-Pinning (@sha) und Toolchain-Pinning (1.85.0) in CI - SA-05: Session-Token vollstaendig aus URIs verbannt (403 Forbidden bei Vorkommen im Pfad/Query) - SA-06: Constant-Time Token- und Auth-Vergleiche via subtle::ConstantTimeEq - SA-07: Dokumentations-Klarstellung bzgl. logischem Shredding vs. physischer SSD/FTL/CoW-Persistenz
This commit is contained in:
+75
-62
@@ -26,14 +26,8 @@ use crate::storage::{Database, NodeRecord};
|
||||
pub fn is_leak_file(filename: &str) -> bool {
|
||||
let lower = filename.trim().to_ascii_lowercase();
|
||||
match lower.as_str() {
|
||||
"thumbs.db"
|
||||
| "ehthumbs.db"
|
||||
| "ehthumbs_vista.db"
|
||||
| "desktop.ini"
|
||||
| "folder.jpg"
|
||||
| "albumartsmall.jpg"
|
||||
| "autorun.inf"
|
||||
| ".ds_store" => true,
|
||||
"thumbs.db" | "ehthumbs.db" | "ehthumbs_vista.db" | "desktop.ini" | "folder.jpg"
|
||||
| "albumartsmall.jpg" | "autorun.inf" | ".ds_store" => true,
|
||||
_ => {
|
||||
if lower.starts_with("albumart") && (lower.ends_with(".jpg") || lower.ends_with(".ini"))
|
||||
{
|
||||
@@ -167,7 +161,6 @@ impl SanctumFile {
|
||||
self.last_activity.store(now, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
|
||||
/// Schreibt den aktuell im RAM gehaltenen Chunk verschlüsselt in die SQLite-Datenbank zurück
|
||||
/// und aktualisiert Dateigröße und Modifikationszeitstempel atomar in einer Transaktion (CHAOS-01).
|
||||
/// Bei Fehlern (z. B. Disk Full) wird der Cache sauber invalidiert (CHAOS-03).
|
||||
@@ -179,10 +172,12 @@ impl SanctumFile {
|
||||
|
||||
if let Some((idx, ref data, true)) = self.cached_chunk {
|
||||
let (ciphertext, nonce, tag) =
|
||||
encrypt_chunk(&self.dek, self.node_id, idx, data, self.format_version).map_err(|e| {
|
||||
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
||||
FsError::GeneralFailure
|
||||
})?;
|
||||
encrypt_chunk(&self.dek, self.node_id, idx, data, self.format_version).map_err(
|
||||
|e| {
|
||||
error!("Verschlüsselungsfehler beim Chunk-Flush: {e}");
|
||||
FsError::GeneralFailure
|
||||
},
|
||||
)?;
|
||||
|
||||
if let Err(e) = self.db.write_chunk_and_update_size(
|
||||
self.node_id,
|
||||
@@ -207,7 +202,10 @@ impl SanctumFile {
|
||||
self.meta.modified_at = UNIX_EPOCH + Duration::from_secs(now);
|
||||
} else if self.meta.size != self.file_size {
|
||||
// Falls kein Chunk dirty war, aber sich z. B. die Dateigröße durch Truncate geändert hat
|
||||
if let Err(e) = self.db.update_node_size_and_time(self.node_id, self.file_size, now) {
|
||||
if let Err(e) = self
|
||||
.db
|
||||
.update_node_size_and_time(self.node_id, self.file_size, now)
|
||||
{
|
||||
error!("Fehler beim Aktualisieren der Knotengröße: {e}");
|
||||
return Err(FsError::GeneralFailure);
|
||||
}
|
||||
@@ -263,7 +261,10 @@ impl SanctumFile {
|
||||
impl Drop for SanctumFile {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = self.flush_cached_chunk_and_size() {
|
||||
warn!("Fehler beim automatischen Flush im SanctumFile::drop: {:?}", e);
|
||||
warn!(
|
||||
"Fehler beim automatischen Flush im SanctumFile::drop: {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
if let Some((_, ref mut data, _)) = self.cached_chunk {
|
||||
data.zeroize();
|
||||
@@ -354,7 +355,12 @@ impl DavFile for SanctumFile {
|
||||
}
|
||||
|
||||
// Wenn der Chunk exakt 1 MB erreicht hat, sofort flushen, um RAM zu schonen
|
||||
if self.cached_chunk.as_ref().map(|(_, d, _)| d.len() >= CHUNK_SIZE).unwrap_or(false) {
|
||||
if self
|
||||
.cached_chunk
|
||||
.as_ref()
|
||||
.map(|(_, d, _)| d.len() >= CHUNK_SIZE)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.flush_cached_chunk_and_size()?;
|
||||
}
|
||||
|
||||
@@ -633,14 +639,19 @@ impl DavFileSystem for SanctumFs {
|
||||
}
|
||||
|
||||
self.touch();
|
||||
debug!("VFS open aufgerufen: path='{}', options={:?}", path_str, options);
|
||||
debug!(
|
||||
"VFS open aufgerufen: path='{}', options={:?}",
|
||||
path_str, options
|
||||
);
|
||||
|
||||
let existing_node = self.resolve_path(&path_str)?;
|
||||
|
||||
let node = match existing_node {
|
||||
Some(n) => {
|
||||
// Schutz der Trägerdatei im Decoy Vault: Keine Schreib- oder Truncate-Operationen erlaubt!
|
||||
if self.carrier_node_id == Some(n.id) && (options.write || options.truncate || options.append) {
|
||||
if self.carrier_node_id == Some(n.id)
|
||||
&& (options.write || options.truncate || options.append)
|
||||
{
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
|
||||
@@ -674,9 +685,7 @@ impl DavFileSystem for SanctumFs {
|
||||
}
|
||||
None => {
|
||||
if options.create || options.create_new {
|
||||
let parent = self
|
||||
.resolve_path(parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let parent = self.resolve_path(parent_path)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
@@ -712,9 +721,7 @@ impl DavFileSystem for SanctumFs {
|
||||
|
||||
Box::pin(async move {
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
@@ -749,9 +756,7 @@ impl DavFileSystem for SanctumFs {
|
||||
|
||||
Box::pin(async move {
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
let meta = SanctumMetaData {
|
||||
is_dir: node.is_dir,
|
||||
@@ -789,9 +794,7 @@ impl DavFileSystem for SanctumFs {
|
||||
return Err(FsError::Exists);
|
||||
}
|
||||
|
||||
let parent = self
|
||||
.resolve_path(parent_path)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let parent = self.resolve_path(parent_path)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !parent.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
@@ -811,9 +814,7 @@ impl DavFileSystem for SanctumFs {
|
||||
Box::pin(async move {
|
||||
self.touch();
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
if !node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
@@ -826,7 +827,11 @@ impl DavFileSystem for SanctumFs {
|
||||
|
||||
// Schutz der Trägerdatei im Decoy Vault: Verzeichnis darf nicht gelöscht werden, wenn es den Carrier enthält!
|
||||
if let Some(carrier_id) = self.carrier_node_id {
|
||||
if self.db.is_descendant_of(carrier_id, node.id).map_err(|_| FsError::GeneralFailure)? {
|
||||
if self
|
||||
.db
|
||||
.is_descendant_of(carrier_id, node.id)
|
||||
.map_err(|_| FsError::GeneralFailure)?
|
||||
{
|
||||
return Err(FsError::Forbidden);
|
||||
}
|
||||
}
|
||||
@@ -847,9 +852,7 @@ impl DavFileSystem for SanctumFs {
|
||||
Box::pin(async move {
|
||||
self.touch();
|
||||
let path_str = Self::path_to_str(path);
|
||||
let node = self
|
||||
.resolve_path(&path_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let node = self.resolve_path(&path_str)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
if node.is_dir {
|
||||
return Err(FsError::Forbidden);
|
||||
@@ -868,11 +871,7 @@ impl DavFileSystem for SanctumFs {
|
||||
})
|
||||
}
|
||||
|
||||
fn rename<'a>(
|
||||
&'a self,
|
||||
from: &'a DavPath,
|
||||
to: &'a DavPath,
|
||||
) -> FsFuture<'a, ()> {
|
||||
fn rename<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
|
||||
if let Some(ref cfs) = self.carrier_fs {
|
||||
return cfs.rename(from, to);
|
||||
}
|
||||
@@ -882,9 +881,7 @@ impl DavFileSystem for SanctumFs {
|
||||
let from_str = Self::path_to_str(from);
|
||||
let to_str = Self::path_to_str(to);
|
||||
|
||||
let node = self
|
||||
.resolve_path(&from_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
// Schutz der Trägerdatei im Decoy Vault: Umbenennen verboten!
|
||||
if self.carrier_node_id == Some(node.id) {
|
||||
@@ -925,19 +922,13 @@ impl DavFileSystem for SanctumFs {
|
||||
})
|
||||
}
|
||||
|
||||
fn copy<'a>(
|
||||
&'a self,
|
||||
from: &'a DavPath,
|
||||
to: &'a DavPath,
|
||||
) -> FsFuture<'a, ()> {
|
||||
fn copy<'a>(&'a self, from: &'a DavPath, to: &'a DavPath) -> FsFuture<'a, ()> {
|
||||
Box::pin(async move {
|
||||
self.touch();
|
||||
let from_str = Self::path_to_str(from);
|
||||
let to_str = Self::path_to_str(to);
|
||||
|
||||
let node = self
|
||||
.resolve_path(&from_str)?
|
||||
.ok_or(FsError::NotFound)?;
|
||||
let node = self.resolve_path(&from_str)?.ok_or(FsError::NotFound)?;
|
||||
|
||||
if node.is_dir {
|
||||
return Err(FsError::NotImplemented);
|
||||
@@ -991,9 +982,14 @@ impl DavFileSystem for SanctumFs {
|
||||
)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
let (new_ct, new_nonce, new_tag) =
|
||||
encrypt_chunk(&self.dek, dest_node.id, idx, &plaintext, self.format_version)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
let (new_ct, new_nonce, new_tag) = encrypt_chunk(
|
||||
&self.dek,
|
||||
dest_node.id,
|
||||
idx,
|
||||
&plaintext,
|
||||
self.format_version,
|
||||
)
|
||||
.map_err(|_| FsError::GeneralFailure)?;
|
||||
|
||||
self.db
|
||||
.write_chunk(dest_node.id, idx, &new_nonce, &new_tag, &new_ct)
|
||||
@@ -1025,7 +1021,9 @@ impl DavFileSystem for SanctumFs {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::crypto::{derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION};
|
||||
use crate::crypto::{
|
||||
derive_kek, generate_dek, generate_salt, wrap_dek, KdfParams, FORMAT_VERSION,
|
||||
};
|
||||
use dav_server::fs::OpenOptions;
|
||||
use futures_util::StreamExt;
|
||||
|
||||
@@ -1078,7 +1076,8 @@ mod tests {
|
||||
pub struct TempDir(PathBuf);
|
||||
impl TempDir {
|
||||
pub fn new() -> Self {
|
||||
let p = std::env::temp_dir().join(format!("sanctum_test_{}", rand::random::<u64>()));
|
||||
let p =
|
||||
std::env::temp_dir().join(format!("sanctum_test_{}", rand::random::<u64>()));
|
||||
std::fs::create_dir_all(&p).unwrap();
|
||||
Self(p)
|
||||
}
|
||||
@@ -1137,7 +1136,10 @@ mod tests {
|
||||
|
||||
// read_dir mit anti_leak = true darf Thumbs.db NICHT anzeigen
|
||||
let root_path = DavPath::new("/").unwrap();
|
||||
let mut stream = fs_shielded.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
||||
let mut stream = fs_shielded
|
||||
.read_dir(&root_path, ReadDirMeta::None)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut names = Vec::new();
|
||||
while let Some(entry) = stream.next().await {
|
||||
let entry = entry.unwrap();
|
||||
@@ -1154,7 +1156,10 @@ mod tests {
|
||||
FORMAT_VERSION,
|
||||
false,
|
||||
);
|
||||
let mut stream_unshielded = fs_unshielded.read_dir(&root_path, ReadDirMeta::None).await.unwrap();
|
||||
let mut stream_unshielded = fs_unshielded
|
||||
.read_dir(&root_path, ReadDirMeta::None)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut names_unshielded = Vec::new();
|
||||
while let Some(entry) = stream_unshielded.next().await {
|
||||
let entry = entry.unwrap();
|
||||
@@ -1263,7 +1268,13 @@ mod tests {
|
||||
|
||||
// 1. Null-Byte im Pfad
|
||||
let null_path = DavPath::new("/bad\0file.txt");
|
||||
assert!(null_path.is_err() || fs.open(&null_path.unwrap(), OpenOptions::default()).await.is_err());
|
||||
assert!(
|
||||
null_path.is_err()
|
||||
|| fs
|
||||
.open(&null_path.unwrap(), OpenOptions::default())
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(validate_path_safety("/bad\0file.txt").is_err());
|
||||
|
||||
// 2. Steuerzeichen < 0x20
|
||||
@@ -1288,7 +1299,9 @@ mod tests {
|
||||
|
||||
// 1. Schreibe 500 Bytes
|
||||
let payload = Bytes::from(vec![42u8; 500]);
|
||||
file.write_buf(Box::new(std::io::Cursor::new(payload))).await.unwrap();
|
||||
file.write_buf(Box::new(std::io::Cursor::new(payload)))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 2. Expliziter Flush: muss Chunk & Dateigröße atomar persistieren
|
||||
file.flush().await.unwrap();
|
||||
|
||||
Reference in New Issue
Block a user