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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// S-08 Path Traversal Guard:
|
// S-02 & S-08 Path Traversal Guard:
|
||||||
// Prüfe Komponenten des relativen Pfads und stelle sicher, dass der Pfad nicht ausbricht
|
// Plattformunabhängiger Pfadaufbau mit Segment-Validierung
|
||||||
for comp in Path::new(&child_rel).components() {
|
let mut local_child_path = local_target_base.to_path_buf();
|
||||||
match comp {
|
for seg in child_rel.split('/') {
|
||||||
std::path::Component::Normal(_) => {}
|
if seg.is_empty() || seg == "." || seg == ".." || seg.contains('\\') {
|
||||||
_ => bail!(
|
bail!(
|
||||||
"Path traversal Versuch erkannt in relativem Pfad: '{}'",
|
"Path traversal Versuch erkannt in relativem Pfad: '{}'",
|
||||||
child_rel
|
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) {
|
if !local_child_path.starts_with(local_target_base) {
|
||||||
bail!(
|
bail!(
|
||||||
"Path traversal Versuch erkannt: '{}' bricht aus Zielverzeichnis aus",
|
"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 child.is_dir {
|
||||||
if !options.dry_run {
|
if !options.dry_run {
|
||||||
fs::create_dir_all(&local_child_path)?;
|
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",
|
b"Album Art or Explorer Cache",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
fs::write(
|
fs::write(restore_dir.join("local_notes.pdf"), b"Private Local Notes").unwrap();
|
||||||
restore_dir.join("local_notes.pdf"),
|
|
||||||
b"Private Local Notes",
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let (db, dek) = create_test_container(&container_path);
|
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);
|
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