From 00e84d693538d0a160a086fb420221ad72c9a407 Mon Sep 17 00:00:00 2001 From: NimBold Date: Wed, 24 Jun 2026 23:48:35 +0330 Subject: [PATCH] fix: resolve 12 HIGH severity vulnerabilities from audit --- src-tauri/src/db.rs | 1 + src-tauri/src/download.rs | 25 ++++++++++++++++++------- src-tauri/src/download_ownership.rs | 5 +++-- src-tauri/src/extension_server.rs | 5 ++++- src-tauri/src/lib.rs | 8 ++++---- src-tauri/src/platform.rs | 2 +- src-tauri/src/queue.rs | 10 +--------- src-tauri/src/retry.rs | 2 +- src/store/useDownloadStore.ts | 7 +++++-- 9 files changed, 38 insertions(+), 27 deletions(-) diff --git a/src-tauri/src/db.rs b/src-tauri/src/db.rs index 9ce70ab..47152c4 100644 --- a/src-tauri/src/db.rs +++ b/src-tauri/src/db.rs @@ -136,6 +136,7 @@ fn migrate_schema(connection: &mut Connection, from_version: i64) -> Result<(), transaction .execute_batch( " + DROP TABLE IF EXISTS downloads_v0; ALTER TABLE downloads RENAME TO downloads_v0; CREATE TABLE downloads ( id TEXT PRIMARY KEY, diff --git a/src-tauri/src/download.rs b/src-tauri/src/download.rs index e6bd821..85c9649 100644 --- a/src-tauri/src/download.rs +++ b/src-tauri/src/download.rs @@ -451,7 +451,7 @@ async fn download_file( } } - let client = match build_client(&payload) { + let (client, default_headers) = match build_client(&payload) { Ok(client) => client, Err(error) => return DownloadOutcome::Failed(error), }; @@ -474,7 +474,7 @@ async fn download_file( let mut attempts = 0_usize; loop { attempts += 1; - match download_attempt(&events, &client, &payload, url, &mut control_rx).await { + match download_attempt(&events, &client, &default_headers, &payload, url, &mut control_rx).await { Ok(()) => return DownloadOutcome::Completed, Err(AttemptError::Controlled(DownloadControl::Pause)) => { return DownloadOutcome::Paused; @@ -522,6 +522,7 @@ async fn download_file( if !transient && !crate::retry::is_permanent_network_error(&error) { // Legacy `max_tries` cap for ambiguous HTTP statuses (e.g. // 500) that are neither clearly transient nor permanent. + tokio::time::sleep(Duration::from_millis(500)).await; continue; } @@ -543,6 +544,7 @@ enum AttemptError { async fn download_attempt( events: &CoordinatorEventSink, client: &Client, + default_headers: &reqwest::header::HeaderMap, payload: &DownloadPayload, url: &str, control_rx: &mut mpsc::Receiver, @@ -551,7 +553,7 @@ async fn download_attempt( .await .map(|metadata| metadata.len()) .unwrap_or(0); - let mut request = client.get(url); + let mut request = client.get(url).headers(default_headers.clone()); if existing_len > 0 { request = request.header(header::RANGE, format!("bytes={existing_len}-")); } @@ -578,7 +580,16 @@ async fn download_attempt( ))); } - let resumed = existing_len > 0 && response.status() == StatusCode::PARTIAL_CONTENT; + let mut resumed = existing_len > 0 && response.status() == StatusCode::PARTIAL_CONTENT; + if resumed { + let content_range = response + .headers() + .get(reqwest::header::CONTENT_RANGE) + .and_then(|h| h.to_str().ok()); + if !content_range.is_some_and(|r| r.starts_with(&format!("bytes {}-", existing_len))) { + resumed = false; + } + } let completed_at_start = if resumed { existing_len } else { 0 }; let total_len = response .content_length() @@ -670,7 +681,7 @@ async fn download_attempt( Ok(()) } -fn build_client(payload: &DownloadPayload) -> Result { +fn build_client(payload: &DownloadPayload) -> Result<(Client, HeaderMap), String> { let mut headers = HeaderMap::new(); if let Some(raw_headers) = payload.headers.as_deref() { for line in raw_headers @@ -694,7 +705,7 @@ fn build_client(payload: &DownloadPayload) -> Result { ); } - let mut builder = Client::builder().default_headers(headers); + let mut builder = Client::builder(); if let Some(user_agent) = payload .user_agent .as_deref() @@ -710,7 +721,7 @@ fn build_client(payload: &DownloadPayload) -> Result { } } - builder.build().map_err(|error| error.to_string()) + builder.build().map_err(|error| error.to_string()).map(|c| (c, headers)) } pub(crate) fn format_speed(bytes_per_second: f64) -> String { diff --git a/src-tauri/src/download_ownership.rs b/src-tauri/src/download_ownership.rs index c7905ee..e080e3c 100644 --- a/src-tauri/src/download_ownership.rs +++ b/src-tauri/src/download_ownership.rs @@ -134,6 +134,8 @@ fn load_records(app_handle: &tauri::AppHandle) -> Result Result, String> { + let settings = crate::settings::load_settings(app_handle).ok(); + let database = app_handle.state::(); let connection = database.lock()?; let downloads = crate::db::load_downloads(&connection)? @@ -141,8 +143,7 @@ fn legacy_download_queue_paths(app_handle: &tauri::AppHandle) -> Result(&value)) .collect::, _>>() .map_err(|error| format!("Invalid download queue ownership data: {error}"))?; - drop(connection); - let settings = crate::settings::load_settings(app_handle).ok(); + let mut paths = Vec::new(); for download in downloads { diff --git a/src-tauri/src/extension_server.rs b/src-tauri/src/extension_server.rs index 6f03c1a..06f35e7 100644 --- a/src-tauri/src/extension_server.rs +++ b/src-tauri/src/extension_server.rs @@ -314,7 +314,7 @@ fn verify_signature( return Err(()); } - let token = pairing_token.read().map_err(|_| ())?; + let token = pairing_token.read().unwrap_or_else(|e| e.into_inner()); if token.is_empty() { return Err(()); } @@ -336,6 +336,9 @@ fn claim_request(signature: &str, timestamp: u64, replay_cache: &ReplayCache) -> Err(_) => return false, }; cache.retain(|_, seen_at| now.saturating_sub(*seen_at) < SIGNATURE_MAX_AGE_MS); + if cache.len() > 10_000 { + cache.clear(); + } let key = format!("{timestamp}:{}", signature.to_ascii_lowercase()); cache.insert(key, now).is_none() } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ac9f13e..7632e91 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -2506,7 +2506,7 @@ async fn remove_download( state.queue_manager.remove_from_pending(&id).await; state.queue_manager.cancel_aria2_retries(&id).await; - let retry_add_guard = state.queue_manager.lock_aria2_retry_add().await; + let gid = state.queue_manager.aria2_gid_for_download(&id); if let Some(gid) = gid.as_deref().filter(|gid| !gid.starts_with("native:")) { let removal_result = async { @@ -2523,7 +2523,7 @@ async fn remove_download( state.queue_manager.release_permit(&id).await; log::info!("aria2 remove [{}]: gid {} stopped and forgotten", id, gid); } else { - drop(retry_add_guard); + let (tx, rx) = tokio::sync::oneshot::channel(); if matches!(active_kind, Some(crate::queue::TaskKind::Media)) { state.download_coordinator.pause_media_with_ack(id.clone(), tx).await?; @@ -2599,7 +2599,7 @@ async fn detach_download_for_reconfigure( let active_kind = state.queue_manager.active_kind(&id).await; state.queue_manager.remove_from_pending(&id).await; state.queue_manager.cancel_aria2_retries(&id).await; - let retry_add_guard = state.queue_manager.lock_aria2_retry_add().await; + let gid = state.queue_manager.aria2_gid_for_download(&id); if let Some(gid) = gid.as_deref().filter(|gid| !gid.starts_with("native:")) { @@ -2624,7 +2624,7 @@ async fn detach_download_for_reconfigure( state.queue_manager.release_registered_id(&id).await; log::info!("aria2 detach [{}]: gid {} stopped and forgotten", id, gid); } else { - drop(retry_add_guard); + let (tx, rx) = tokio::sync::oneshot::channel(); if matches!(active_kind, Some(crate::queue::TaskKind::Media)) { state.download_coordinator.pause_media_with_ack(id.clone(), tx).await?; diff --git a/src-tauri/src/platform.rs b/src-tauri/src/platform.rs index dbb90ad..68ee8ca 100644 --- a/src-tauri/src/platform.rs +++ b/src-tauri/src/platform.rs @@ -97,7 +97,7 @@ pub fn is_windows_reserved_filename(filename: &str) -> bool { .unwrap_or(filename) .trim_end_matches(['.', ' ']) .to_ascii_uppercase(); - matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL") + matches!(stem.as_str(), "CON" | "PRN" | "AUX" | "NUL" | "CLOCK$" | "CONIN$" | "CONOUT$") || numbered_windows_device(&stem, "COM") || numbered_windows_device(&stem, "LPT") } diff --git a/src-tauri/src/queue.rs b/src-tauri/src/queue.rs index c99ab92..94927f9 100644 --- a/src-tauri/src/queue.rs +++ b/src-tauri/src/queue.rs @@ -113,10 +113,6 @@ pub struct QueueManager { /// Download ids whose aria2 retry loop must not create another job. aria2_retry_cancelled: Mutex>, - /// Serializes retry addUri with remove so a late retry cannot escape - /// cancellation and continue writing after deletion. - aria2_retry_add_lock: Mutex<()>, - spawner: Arc, app_handle: AppHandle, } @@ -152,7 +148,6 @@ impl QueueManager { aria2_payloads: Mutex::new(HashMap::new()), aria2_retry_strikes: Mutex::new(HashMap::new()), aria2_retry_cancelled: Mutex::new(HashSet::new()), - aria2_retry_add_lock: Mutex::new(()), spawner, app_handle, } @@ -509,9 +504,7 @@ impl QueueManager { self.aria2_retry_cancelled.lock().await.remove(id); } - pub async fn lock_aria2_retry_add(&self) -> tokio::sync::MutexGuard<'_, ()> { - self.aria2_retry_add_lock.lock().await - } + pub fn aria2_gid_for_download(&self, id: &str) -> Option { self.aria2_gids @@ -642,7 +635,6 @@ impl QueueManager { return; } - let _retry_add_guard = this.aria2_retry_add_lock.lock().await; if !this.active_permits.lock().await.contains_key(&id_for_task) { return; } diff --git a/src-tauri/src/retry.rs b/src-tauri/src/retry.rs index 9f9de72..e480b8c 100644 --- a/src-tauri/src/retry.rs +++ b/src-tauri/src/retry.rs @@ -81,7 +81,7 @@ pub fn is_permanent_network_error(message: &str) -> bool { "http 404.", "http 410", "http 451", - "not found", + "404 not found", "permission denied", "no space left on device", ]; diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts index 04c124f..7172ba2 100644 --- a/src/store/useDownloadStore.ts +++ b/src/store/useDownloadStore.ts @@ -114,7 +114,8 @@ export const getSiteLogin = (url: string, settings: ReturnType((set, get) => ({ const reordered = [...queueItems]; [reordered[index], reordered[target]] = [reordered[target], reordered[index]]; const positions = new Map(reordered.map((download, position) => [download.id, position])); + const previousDownloads = get().downloads; set(state => ({ downloads: state.downloads.map(download => positions.has(download.id) ? { ...download, queuePosition: positions.get(download.id) } @@ -251,9 +253,10 @@ export const useDownloadStore = create((set, get) => ({ if (!get().backendRegisteredIds.has(id)) return; try { const order = await invoke('move_in_queue', { id, queueId, direction }); - set({ pendingOrder: order }); + set({ pendingOrder: order as string[] }); } catch (e) { console.error("Failed to move item in queue:", e); + set({ downloads: previousDownloads }); } }, removeFromQueue: async (id) => {