From c96ab0faa00428660f75e47f938d313393bfbd7b Mon Sep 17 00:00:00 2001 From: M1noa Date: Thu, 16 Jul 2026 17:55:27 -0500 Subject: [PATCH] fix(LCEL-01): scope IPC commands to app data dir instead of disabling the previous patch disabled write_binary_file / read_binary_file / list_directory outright, but the frontend actively calls them for plugin loading (PluginManager.ts reads plugin manifests and code from the plugins dir, TauriService.ts wraps them for general use). disabling broke plugin loading. proper fix: scope all three commands to the launcher's app data dir via canonicalize + starts_with check. legitimate callers operate inside this dir anyway (plugin discovery, plugin code loading). the write+launch primitive is closed because the app data dir doesn't contain startup folders, autostart locations, or other sensitive targets. --- src-tauri/src/commands/file_dialogs.rs | 45 ++++++++++++++++++++------ src-tauri/src/commands/plugins.rs | 34 ++++++++++++++++--- 2 files changed, 66 insertions(+), 13 deletions(-) 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]