From 68f1bff93bdca47eae6e1e38edcd9f0de8b3e9eb Mon Sep 17 00:00:00 2001 From: Tristan Michael Date: Thu, 2 Apr 2026 08:23:07 -0700 Subject: [PATCH] fix: prevent path traversal, enable CSP, and harden URL domain extraction Validate that resolved list paths stay within the workspace root to prevent directory traversal via malicious list names. Enable Content Security Policy in Tauri config instead of leaving it null. Fix CLI domain extraction to strip userinfo (user:pass@) from URLs before using as keyring service name. --- apps/tauri/src-tauri/tauri.conf.json | 2 +- crates/onyx-cli/src/commands/sync.rs | 24 ++++++++++++---------- crates/onyx-core/src/storage.rs | 30 ++++++++++++++++++++++++---- 3 files changed, 40 insertions(+), 16 deletions(-) diff --git a/apps/tauri/src-tauri/tauri.conf.json b/apps/tauri/src-tauri/tauri.conf.json index 1613341..83a6a4b 100644 --- a/apps/tauri/src-tauri/tauri.conf.json +++ b/apps/tauri/src-tauri/tauri.conf.json @@ -23,7 +23,7 @@ } ], "security": { - "csp": null + "csp": "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; connect-src ipc: http://ipc.localhost" } }, "bundle": { diff --git a/crates/onyx-cli/src/commands/sync.rs b/crates/onyx-cli/src/commands/sync.rs index 234062d..a20d0c2 100644 --- a/crates/onyx-cli/src/commands/sync.rs +++ b/crates/onyx-cli/src/commands/sync.rs @@ -204,18 +204,20 @@ fn print_workspace_status(name: &str, path: &std::path::Path, webdav_url: Option Ok(()) } -/// Extract domain from a URL for credential storage. +/// Extract host from a URL for credential storage. fn extract_domain(url: &str) -> String { - url.split("://") - .nth(1) - .unwrap_or(url) - .split('/') - .next() - .unwrap_or(url) - .split(':') - .next() - .unwrap_or(url) - .to_string() + // Strip scheme + let after_scheme = url.split("://").nth(1).unwrap_or(url); + // Strip path + let authority = after_scheme.split('/').next().unwrap_or(after_scheme); + // Strip userinfo (user:pass@host) + let host_port = if let Some(at_pos) = authority.rfind('@') { + &authority[at_pos + 1..] + } else { + authority + }; + // Strip port + host_port.split(':').next().unwrap_or(host_port).to_string() } /// Prompt the user for text input. diff --git a/crates/onyx-core/src/storage.rs b/crates/onyx-core/src/storage.rs index 62bb4a2..9a41991 100644 --- a/crates/onyx-core/src/storage.rs +++ b/crates/onyx-core/src/storage.rs @@ -149,8 +149,30 @@ impl FileSystemStorage { Err(Error::ListNotFound(list_id.to_string())) } - fn list_dir_path_by_name(&self, name: &str) -> PathBuf { - self.root_path.join(name) + fn list_dir_path_by_name(&self, name: &str) -> Result { + let path = self.root_path.join(name); + // Prevent path traversal: resolved path must stay within root + let canonical_root = self.root_path.canonicalize() + .unwrap_or_else(|_| self.root_path.clone()); + let canonical_path = if path.exists() { + path.canonicalize().unwrap_or_else(|_| path.clone()) + } else { + // For non-existent paths, normalize by resolving the parent + if let Some(parent) = path.parent() { + let canonical_parent = if parent.exists() { + parent.canonicalize().unwrap_or_else(|_| parent.to_path_buf()) + } else { + parent.to_path_buf() + }; + canonical_parent.join(path.file_name().unwrap_or_default()) + } else { + path.clone() + } + }; + if !canonical_path.starts_with(&canonical_root) { + return Err(Error::InvalidData(format!("Invalid list name: path escapes workspace"))); + } + Ok(path) } fn sanitize_filename(name: &str) -> String { @@ -371,7 +393,7 @@ impl Storage for FileSystemStorage { } fn create_list(&mut self, name: String) -> Result { - let list_dir = self.list_dir_path_by_name(&name); + let list_dir = self.list_dir_path_by_name(&name)?; if list_dir.exists() { return Err(Error::InvalidData(format!("List '{}' already exists", name))); @@ -473,7 +495,7 @@ impl Storage for FileSystemStorage { fn rename_list(&mut self, list_id: Uuid, new_name: String) -> Result<()> { let old_dir = self.list_dir_path(list_id)?; - let new_dir = self.list_dir_path_by_name(&new_name); + let new_dir = self.list_dir_path_by_name(&new_name)?; if new_dir.exists() { return Err(Error::InvalidData(format!("A list named '{}' already exists", new_name)));