fix(sync): S-02 — platform-independent path construction avoids path traversal

This commit is contained in:
2026-09-19 01:01:09 +02:00
parent bdddd812ac
commit 7b0f061221
2 changed files with 115 additions and 15 deletions
+105 -5
View File
@@ -397,11 +397,7 @@ fn test_s01_pull_delete_preserves_local_excluded_and_leak_files() {
b"Album Art or Explorer Cache",
)
.unwrap();
fs::write(
restore_dir.join("local_notes.pdf"),
b"Private Local Notes",
)
.unwrap();
fs::write(restore_dir.join("local_notes.pdf"), b"Private Local Notes").unwrap();
let (db, dek) = create_test_container(&container_path);
@@ -462,3 +458,107 @@ fn test_s01_pull_delete_preserves_local_excluded_and_leak_files() {
let _ = fs::remove_dir_all(&temp_root);
}
#[test]
fn test_s02_platform_independent_path_construction_and_traversal_rejection() {
let temp_root =
std::env::temp_dir().join(format!("sanctum_s02_test_{}", rand::random::<u64>()));
let container_path = temp_root.join("test.sanctum");
let source_dir = temp_root.join("source");
let target_dir = temp_root.join("restored");
fs::create_dir_all(&source_dir).unwrap();
let deep_dir = source_dir.join("level1").join("level2").join("level3");
fs::create_dir_all(&deep_dir).unwrap();
fs::write(deep_dir.join("deep_doc.txt"), b"Deeply nested content").unwrap();
let (db, dek) = create_test_container(&container_path);
// 1. Push: Übertrage mehrstufige Verzeichnisstruktur in den Tresor
let mut push_opts = SyncOptions::default();
push_opts.direction = SyncDirection::Push;
push_opts.quiet = true;
run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
source_dir.to_str().unwrap(),
"/MultiLevel",
&push_opts,
)
.expect("Sync push multi-level");
// 2. Pull: Prüfe, dass mehrstufige Pfade plattformunabhängig rekonstruiert werden
let mut pull_opts = SyncOptions::default();
pull_opts.direction = SyncDirection::Pull;
pull_opts.quiet = true;
run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
"/MultiLevel",
target_dir.to_str().unwrap(),
&pull_opts,
)
.expect("Sync pull multi-level");
let restored_file = target_dir
.join("level1")
.join("level2")
.join("level3")
.join("deep_doc.txt");
assert!(
restored_file.exists(),
"Mehrstufige Datei muss korrekt auf dem Host angelegt worden sein"
);
assert_eq!(fs::read(restored_file).unwrap(), b"Deeply nested content");
// 3. Traversal-Abwehr: Bösartiger Knoten mit relativem Ausbruchsversuch
// Simuliere manipulierten Knoten im Tresor
{
let conn = rusqlite::Connection::open(&container_path).unwrap();
// Erstelle Knoten mit manipuliertem Namen '../evil.txt'
conn.execute(
"INSERT INTO nodes (id, parent_id, name, is_dir, size, created_at, modified_at, is_carrier)
VALUES (9999, 1, '../evil.txt', 0, 10, 100, 100, 0)",
[],
)
.unwrap();
}
let malicious_target = temp_root.join("malicious_target");
fs::create_dir_all(&malicious_target).unwrap();
let pull_res = run_sync(
&db,
0,
&dek,
FORMAT_VERSION,
"/",
malicious_target.to_str().unwrap(),
&pull_opts,
);
assert!(
pull_res.is_err(),
"Sync Pull muss bei manipuliertem Traversal-Pfad abbrechen"
);
let err_msg = pull_res.unwrap_err().to_string();
assert!(
err_msg.contains("Path traversal")
|| err_msg.contains("Ungültiger Dateiname")
|| err_msg.contains("unzulässige Trennzeichen"),
"Fehlermeldung muss Traversal-Erkennung ausweisen: {}",
err_msg
);
assert!(
!temp_root.join("evil.txt").exists(),
"Es darf niemals eine Datei außerhalb des Zielverzeichnisses erzeugt werden"
);
// Aufräumen
let _ = fs::remove_dir_all(&temp_root);
}