fix(sync): S-02 — platform-independent path construction avoids path traversal
This commit is contained in:
+10
-10
@@ -945,21 +945,19 @@ fn collect_and_pull_dir(
|
||||
continue;
|
||||
}
|
||||
|
||||
// S-08 Path Traversal Guard:
|
||||
// Prüfe Komponenten des relativen Pfads und stelle sicher, dass der Pfad nicht ausbricht
|
||||
for comp in Path::new(&child_rel).components() {
|
||||
match comp {
|
||||
std::path::Component::Normal(_) => {}
|
||||
_ => bail!(
|
||||
// S-02 & S-08 Path Traversal Guard:
|
||||
// Plattformunabhängiger Pfadaufbau mit Segment-Validierung
|
||||
let mut local_child_path = local_target_base.to_path_buf();
|
||||
for seg in child_rel.split('/') {
|
||||
if seg.is_empty() || seg == "." || seg == ".." || seg.contains('\\') {
|
||||
bail!(
|
||||
"Path traversal Versuch erkannt in relativem Pfad: '{}'",
|
||||
child_rel
|
||||
),
|
||||
);
|
||||
}
|
||||
local_child_path.push(seg);
|
||||
}
|
||||
|
||||
vault_relative_paths.insert(child_rel.clone());
|
||||
let local_child_path = local_target_base.join(&child_rel.replace('/', "\\"));
|
||||
|
||||
if !local_child_path.starts_with(local_target_base) {
|
||||
bail!(
|
||||
"Path traversal Versuch erkannt: '{}' bricht aus Zielverzeichnis aus",
|
||||
@@ -967,6 +965,8 @@ fn collect_and_pull_dir(
|
||||
);
|
||||
}
|
||||
|
||||
vault_relative_paths.insert(child_rel.clone());
|
||||
|
||||
if child.is_dir {
|
||||
if !options.dry_run {
|
||||
fs::create_dir_all(&local_child_path)?;
|
||||
|
||||
+105
-5
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user