From a12deb518276b02b6c6f3b89413bfc15d4415a7f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Apr 2026 10:17:30 +0000 Subject: [PATCH] Harden codebase: fix security, quality, and maintainability issues - Replace dangerous unwrap() with proper error handling (storage.rs, webdav.rs) - Add atomic writes (temp + rename) for config, sync state, and metadata files - Add path traversal validation in sync executor - Add workspace path validation in Tauri commands - Add input size limits for task titles, descriptions, and list names - Add file download size limit (10MB) to WebDAV get_file - Fix move_task rollback to log failures instead of silently ignoring - Fix JSON serialization unwrap in Tauri create_remote_workspace - Fix swallowed errors in sync queue backup, metadata writes, sync state load - Extract hardcoded strings into named constants (filenames, extensions, limits) - Use REQUEST_TIMEOUT/CONNECT_TIMEOUT constants in WebDAV client builder - Fix frontend: clear taskStack when viewed task is deleted or list is switched - Fix frontend: surface credential loading and focus listener errors https://claude.ai/code/session_01F67yfLLmSaBtT7aKKNus1M --- apps/tauri/src-tauri/src/lib.rs | 31 +++++++- .../src/lib/screens/SettingsScreen.svelte | 4 +- apps/tauri/src/lib/screens/TasksScreen.svelte | 9 ++- apps/tauri/src/lib/stores/app.svelte.ts | 4 +- crates/onyx-core/src/config.rs | 5 +- crates/onyx-core/src/repository.rs | 4 +- crates/onyx-core/src/storage.rs | 74 +++++++++++++++---- crates/onyx-core/src/sync.rs | 57 +++++++++++--- crates/onyx-core/src/webdav.rs | 25 +++++-- 9 files changed, 177 insertions(+), 36 deletions(-) diff --git a/apps/tauri/src-tauri/src/lib.rs b/apps/tauri/src-tauri/src/lib.rs index 7f5d403..e275932 100644 --- a/apps/tauri/src-tauri/src/lib.rs +++ b/apps/tauri/src-tauri/src/lib.rs @@ -41,6 +41,33 @@ fn lock_state(state: &Mutex) -> Result Result<(), String> { + let p = PathBuf::from(path); + // Reject obviously dangerous paths + let normalized = p.to_string_lossy(); + if normalized.is_empty() { + return Err("Workspace path cannot be empty".into()); + } + // Reject paths that are system root directories + #[cfg(unix)] + { + let forbidden = ["/", "/etc", "/usr", "/bin", "/sbin", "/var", "/proc", "/sys", "/dev"]; + let canonical = normalized.trim_end_matches('/'); + if forbidden.contains(&canonical) { + return Err(format!("Cannot use system directory as workspace: {}", path)); + } + } + #[cfg(windows)] + { + let upper = normalized.to_uppercase(); + if upper.len() <= 3 && upper.ends_with(":\\") || upper.ends_with(":") { + return Err(format!("Cannot use drive root as workspace: {}", path)); + } + } + Ok(()) +} + /// Serializable sync result for the frontend. #[derive(Debug, Serialize, Deserialize, Clone)] struct SyncResult { @@ -120,6 +147,7 @@ fn add_workspace( path: String, state: State<'_, Mutex>, ) -> Result<(), String> { + validate_workspace_path(&path)?; let mut s = lock_state(&state)?; let ws = WorkspaceConfig::new(name, PathBuf::from(&path)); let id = s.config.add_workspace(ws); @@ -243,6 +271,7 @@ async fn rename_workspace( #[tauri::command] fn init_workspace(path: String) -> Result<(), String> { + validate_workspace_path(&path)?; TaskRepository::init(PathBuf::from(path)) .map(|_| ()) .map_err(|e| e.to_string()) @@ -625,7 +654,7 @@ async fn create_remote_workspace( } else { format!("{}/{}", path.trim_end_matches('/'), ".onyx-workspace.json") }; - client.put_file(&file_path, serde_json::to_string_pretty(&metadata).unwrap().into_bytes()) + client.put_file(&file_path, serde_json::to_string_pretty(&metadata).map_err(|e| e.to_string())?.into_bytes()) .await .map_err(|e| e.to_string())?; Ok(()) diff --git a/apps/tauri/src/lib/screens/SettingsScreen.svelte b/apps/tauri/src/lib/screens/SettingsScreen.svelte index 17f4958..a56cc53 100644 --- a/apps/tauri/src/lib/screens/SettingsScreen.svelte +++ b/apps/tauri/src/lib/screens/SettingsScreen.svelte @@ -26,7 +26,9 @@ invoke<[string, string]>("load_credentials", { domain }).then(([u, p]) => { webdavUser = u; webdavPass = p; - }).catch(() => {}); + }).catch((e) => { + console.warn("Failed to load credentials:", e); + }); } catch {} }); diff --git a/apps/tauri/src/lib/screens/TasksScreen.svelte b/apps/tauri/src/lib/screens/TasksScreen.svelte index 2f68d3b..bc84859 100644 --- a/apps/tauri/src/lib/screens/TasksScreen.svelte +++ b/apps/tauri/src/lib/screens/TasksScreen.svelte @@ -18,6 +18,13 @@ let parentTask = $derived(taskStack.length >= 1 ? app.tasks.find(t => t.id === taskStack[0]) ?? null : null); let subtaskDetail = $derived(taskStack.length >= 2 ? app.tasks.find(t => t.id === taskStack[1]) ?? null : null); + // Clear taskStack when the viewed task no longer exists (e.g. deleted or list switched) + $effect(() => { + if (taskStack.length > 0 && !parentTask) { + taskStack = []; + } + }); + function openTask(task: Task) { taskStack = [task.id]; } @@ -282,7 +289,7 @@
{#each app.lists as list (list.id)}