fix(sync): S-07 — skip invalid windows filenames with files_skipped_invalid counter
This commit is contained in:
+12
@@ -1493,6 +1493,12 @@ fn handle_sync(
|
|||||||
stats.files_skipped_symlinks
|
stats.files_skipped_symlinks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if stats.files_skipped_invalid > 0 {
|
||||||
|
println!(
|
||||||
|
" • Ungültige Namen: {} übersprungen",
|
||||||
|
stats.files_skipped_invalid
|
||||||
|
);
|
||||||
|
}
|
||||||
if delete {
|
if delete {
|
||||||
println!(
|
println!(
|
||||||
" • Zu löschen: {} Dateien/Ordner",
|
" • Zu löschen: {} Dateien/Ordner",
|
||||||
@@ -1526,6 +1532,12 @@ fn handle_sync(
|
|||||||
stats.files_skipped_symlinks
|
stats.files_skipped_symlinks
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if stats.files_skipped_invalid > 0 {
|
||||||
|
println!(
|
||||||
|
" • Ungültige Namen: {} übersprungen",
|
||||||
|
stats.files_skipped_invalid
|
||||||
|
);
|
||||||
|
}
|
||||||
if delete {
|
if delete {
|
||||||
println!(
|
println!(
|
||||||
" • Gelöscht: {} verwaiste Dateien/Ordner",
|
" • Gelöscht: {} verwaiste Dateien/Ordner",
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
use anyhow::{bail, Result};
|
use anyhow::{bail, Result};
|
||||||
|
|
||||||
|
/// Prüft, ob ein Name Path-Traversal-Muster oder Pfadtrennzeichen enthält (S-02).
|
||||||
|
pub fn is_path_traversal(name: &str) -> bool {
|
||||||
|
name.is_empty()
|
||||||
|
|| name == "."
|
||||||
|
|| name == ".."
|
||||||
|
|| name.contains('/')
|
||||||
|
|| name.contains('\\')
|
||||||
|
|| name.contains('\0')
|
||||||
|
}
|
||||||
|
|
||||||
/// Validiert einen Datei- oder Verzeichnisnamen gegen Path-Traversal, Null-Bytes,
|
/// Validiert einen Datei- oder Verzeichnisnamen gegen Path-Traversal, Null-Bytes,
|
||||||
/// unzulässige Steuerzeichen und Windows-reservierte Gerätenamen (S-08, R-03).
|
/// unzulässige Steuerzeichen und Windows-reservierte Gerätenamen (S-08, R-03).
|
||||||
pub fn validate_node_name(name: &str) -> Result<()> {
|
pub fn validate_node_name(name: &str) -> Result<()> {
|
||||||
|
|||||||
+43
-3
@@ -13,7 +13,7 @@ use crate::storage::{Database, NodeRecord};
|
|||||||
use crate::ui;
|
use crate::ui;
|
||||||
use crate::vfs::is_leak_file;
|
use crate::vfs::is_leak_file;
|
||||||
|
|
||||||
pub use crate::pathutil::validate_node_name;
|
pub use crate::pathutil::{is_path_traversal, validate_node_name};
|
||||||
|
|
||||||
/// Berechnet den SHA-256 Hash einer lokalen Datei für verlässliche Checksummen-Vergleiche (S-10).
|
/// Berechnet den SHA-256 Hash einer lokalen Datei für verlässliche Checksummen-Vergleiche (S-10).
|
||||||
fn calc_local_file_sha256(path: &Path) -> Result<String> {
|
fn calc_local_file_sha256(path: &Path) -> Result<String> {
|
||||||
@@ -108,6 +108,7 @@ pub struct SyncStats {
|
|||||||
pub files_transferred: usize,
|
pub files_transferred: usize,
|
||||||
pub files_skipped: usize,
|
pub files_skipped: usize,
|
||||||
pub files_skipped_symlinks: usize,
|
pub files_skipped_symlinks: usize,
|
||||||
|
pub files_skipped_invalid: usize,
|
||||||
pub files_deleted: usize,
|
pub files_deleted: usize,
|
||||||
pub bytes_transferred: u64,
|
pub bytes_transferred: u64,
|
||||||
pub elapsed: Duration,
|
pub elapsed: Duration,
|
||||||
@@ -645,7 +646,26 @@ fn collect_and_push_dir(
|
|||||||
let entry = entry?;
|
let entry = entry?;
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
validate_node_name(&file_name)?;
|
if is_path_traversal(&file_name) {
|
||||||
|
bail!(
|
||||||
|
"Path traversal Versuch erkannt in Dateinamen: '{}'",
|
||||||
|
file_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Err(e) = validate_node_name(&file_name) {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
stats.files_skipped_invalid += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(
|
||||||
|
" {} Ungültiger Dateiname übersprungen: {} ({})",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
file_name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let rel_path = path
|
let rel_path = path
|
||||||
.strip_prefix(base_dir)
|
.strip_prefix(base_dir)
|
||||||
@@ -991,7 +1011,27 @@ fn collect_and_pull_dir(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_node_name(&child.name)?;
|
if is_path_traversal(&child.name) {
|
||||||
|
bail!(
|
||||||
|
"Path traversal Versuch erkannt in Knotennamen: '{}'",
|
||||||
|
child.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Err(e) = validate_node_name(&child.name) {
|
||||||
|
stats.files_scanned += 1;
|
||||||
|
stats.files_skipped += 1;
|
||||||
|
stats.files_skipped_invalid += 1;
|
||||||
|
if !options.quiet {
|
||||||
|
println!(
|
||||||
|
" {} Ungültiger Dateiname im Tresor übersprungen: {} ({})",
|
||||||
|
ui::yellow("[!]"),
|
||||||
|
child.name,
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let child_rel = if rel_prefix.is_empty() {
|
let child_rel = if rel_prefix.is_empty() {
|
||||||
child.name.clone()
|
child.name.clone()
|
||||||
|
|||||||
@@ -903,3 +903,85 @@ fn test_s06_advisory_lock_blocks_sync_and_force_overrides() {
|
|||||||
|
|
||||||
let _ = fs::remove_dir_all(&temp_root);
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_s07_skip_invalid_windows_filenames_push_and_pull() {
|
||||||
|
let temp_root =
|
||||||
|
std::env::temp_dir().join(format!("sanctum_s07_test_{}", rand::random::<u64>()));
|
||||||
|
let container_path = temp_root.join("s07.sanctum");
|
||||||
|
let source_dir = temp_root.join("source");
|
||||||
|
let restore_dir = temp_root.join("restore");
|
||||||
|
|
||||||
|
fs::create_dir_all(&source_dir).unwrap();
|
||||||
|
fs::create_dir_all(&restore_dir).unwrap();
|
||||||
|
|
||||||
|
let (db, dek) = create_test_container(&container_path);
|
||||||
|
|
||||||
|
// Erstelle valide Dateien
|
||||||
|
fs::write(source_dir.join("valid1.txt"), b"Content 1").unwrap();
|
||||||
|
fs::write(source_dir.join("valid2.txt"), b"Content 2").unwrap();
|
||||||
|
|
||||||
|
// Versuche eine Datei mit ungültigem Namen anzulegen
|
||||||
|
// Unter Windows via extended path r"\\?\"
|
||||||
|
let extended_aux = format!(r"\\?\{}\aux.txt", source_dir.display());
|
||||||
|
let created_aux = fs::write(&extended_aux, b"reserved aux file").is_ok();
|
||||||
|
|
||||||
|
let mut opts = SyncOptions::default();
|
||||||
|
opts.direction = SyncDirection::Push;
|
||||||
|
opts.quiet = true;
|
||||||
|
|
||||||
|
// 1. Push: Darf bei Vorhandensein von aux.txt nicht abbrechen
|
||||||
|
let push_stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
source_dir.to_str().unwrap(),
|
||||||
|
"/",
|
||||||
|
&opts,
|
||||||
|
)
|
||||||
|
.expect("Push sync must not abort when encountering invalid filenames");
|
||||||
|
|
||||||
|
assert_eq!(push_stats.files_transferred, 2);
|
||||||
|
if created_aux {
|
||||||
|
assert!(push_stats.files_skipped_invalid >= 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Jetzt fügen wir manuell einen ungültigen Knoten in die Datenbank ein
|
||||||
|
// (z. B. von Linux oder externem Container importiert: "aux.txt")
|
||||||
|
{
|
||||||
|
let conn = rusqlite::Connection::open(&container_path).unwrap();
|
||||||
|
let root_id = Database::get_root_node_id_for_vault(0);
|
||||||
|
let now = 123456789;
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO nodes (parent_id, name, is_dir, size, created_at, modified_at)
|
||||||
|
VALUES (?1, ?2, 0, 0, ?3, ?4)",
|
||||||
|
rusqlite::params![root_id, "aux.txt", now, now],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Pull: Darf bei unzulässigem Knotennamen im Tresor nicht abbrechen
|
||||||
|
opts.direction = SyncDirection::Pull;
|
||||||
|
let pull_stats = run_sync(
|
||||||
|
&db,
|
||||||
|
0,
|
||||||
|
&dek,
|
||||||
|
FORMAT_VERSION,
|
||||||
|
"/",
|
||||||
|
restore_dir.to_str().unwrap(),
|
||||||
|
&opts,
|
||||||
|
)
|
||||||
|
.expect("Pull sync must not abort when encountering invalid filenames in vault");
|
||||||
|
|
||||||
|
// Valide Dateien müssen wiederhergestellt worden sein
|
||||||
|
assert!(restore_dir.join("valid1.txt").exists());
|
||||||
|
assert!(restore_dir.join("valid2.txt").exists());
|
||||||
|
assert_eq!(pull_stats.files_transferred, 2);
|
||||||
|
assert_eq!(pull_stats.files_skipped_invalid, 1);
|
||||||
|
|
||||||
|
if created_aux {
|
||||||
|
let _ = fs::remove_file(&extended_aux);
|
||||||
|
}
|
||||||
|
let _ = fs::remove_dir_all(&temp_root);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user