diff --git a/src-tauri/src/commands/file_dialogs.rs b/src-tauri/src/commands/file_dialogs.rs index 16003c6..bc505eb 100644 --- a/src-tauri/src/commands/file_dialogs.rs +++ b/src-tauri/src/commands/file_dialogs.rs @@ -44,16 +44,43 @@ pub fn save_file_dialog(title: String, filename: String, filters: Vec) - // paths with no validation, no dialog, no scope. combined with the // opener:allow-open-path: ** capability, a compromised webview could // write+launch any file anywhere. (LCEL-01) -// TODO: scope these to a launcher-specific subdirectory with canonical -// containment checks, or remove from the IPC surface entirely and route -// all binary file ops through rfd-gated dialogs. -// for now, refuse all paths. callers must use pick_file / save_file_dialog. -#[tauri::command] -pub fn write_binary_file(_path: String, _data: Vec) -> Result<(), String> { - Err("write_binary_file disabled: use save_file_dialog instead".into()) +// scope to the launcher's app data dir. legitimate callers (plugin +// loading) operate inside this dir anyway. +use tauri::Manager; + +fn is_path_scoped(path: &std::path::Path, app: &tauri::AppHandle) -> bool { + let canonical = match std::fs::canonicalize(path) { + Ok(p) => p, + Err(_) => return false, + }; + if let Ok(app_dir) = app.path().app_data_dir() { + let app_canonical = std::fs::canonicalize(&app_dir).unwrap_or(app_dir); + return canonical.starts_with(&app_canonical); + } + false } #[tauri::command] -pub fn read_binary_file(_path: String) -> Result, String> { - Err("read_binary_file disabled: use pick_file instead".into()) +pub fn write_binary_file( + app: tauri::AppHandle, + path: String, + data: Vec, +) -> Result<(), String> { + let p = std::path::Path::new(&path); + if !is_path_scoped(p, &app) { + return Err(format!("path outside app dir: {}", path)); + } + fs::write(path, data).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub fn read_binary_file( + app: tauri::AppHandle, + path: String, +) -> Result, String> { + let p = std::path::Path::new(&path); + if !is_path_scoped(p, &app) { + return Err(format!("path outside app dir: {}", path)); + } + fs::read(path).map_err(|e| e.to_string()) } diff --git a/src-tauri/src/commands/plugins.rs b/src-tauri/src/commands/plugins.rs index 91fa33c..bb80be2 100644 --- a/src-tauri/src/commands/plugins.rs +++ b/src-tauri/src/commands/plugins.rs @@ -14,14 +14,40 @@ pub fn get_plugins_dir(app: tauri::AppHandle) -> Result { } #[tauri::command] -pub fn list_directory(path: String) -> Result, String> { +pub fn list_directory( + app: tauri::AppHandle, + path: String, +) -> Result, String> { // security: list_directory used to accept arbitrary paths with no // validation. combined with write_binary_file + opener:allow-open-path: ** // a compromised webview could enumerate startup folders, drop a binary, // and launch it. (LCEL-01) - // TODO: scope to the launcher's plugins dir or remove from IPC entirely. - // for now, refuse all paths. - Err("list_directory disabled: use get_plugins_dir instead".into()) + // scope to the launcher's app data dir. legitimate callers (plugin + // discovery) operate inside this dir anyway. + use tauri::Manager; + let p = std::path::Path::new(&path); + let canonical = match std::fs::canonicalize(p) { + Ok(c) => c, + Err(e) => return Err(format!("path not found: {}", e)), + }; + if let Ok(app_dir) = app.path().app_data_dir() { + let app_canonical = std::fs::canonicalize(&app_dir).unwrap_or(app_dir); + if !canonical.starts_with(&app_canonical) { + return Err(format!("path outside app dir: {}", path)); + } + } else { + return Err("no app dir".into()); + } + let entries = fs::read_dir(&path).map_err(|e| e.to_string())?; + let mut results = Vec::new(); + for entry in entries { + let entry = entry.map_err(|e| e.to_string())?; + results.push(DirEntry { + name: entry.file_name().to_string_lossy().to_string(), + is_dir: entry.file_type().map(|t| t.is_dir()).unwrap_or(false), + }); + } + Ok(results) } #[tauri::command]