fix: resolve 12 HIGH severity vulnerabilities from audit

This commit is contained in:
NimBold
2026-06-24 23:48:35 +03:30
parent b147ec5f78
commit 00e84d6935
9 changed files with 38 additions and 27 deletions
+1
View File
@@ -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,
+18 -7
View File
@@ -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<DownloadControl>,
@@ -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<Client, String> {
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<Client, String> {
);
}
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<Client, String> {
}
}
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 {
+3 -2
View File
@@ -134,6 +134,8 @@ fn load_records(app_handle: &tauri::AppHandle) -> Result<Vec<DownloadOwnershipRe
}
fn legacy_download_queue_paths(app_handle: &tauri::AppHandle) -> Result<Vec<PathBuf>, String> {
let settings = crate::settings::load_settings(app_handle).ok();
let database = app_handle.state::<crate::db::DbState>();
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<Vec<Path
.map(|value| serde_json::from_str::<crate::ipc::DownloadItem>(&value))
.collect::<Result<Vec<_>, _>>()
.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 {
+4 -1
View File
@@ -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()
}
+4 -4
View File
@@ -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?;
+1 -1
View File
@@ -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")
}
+1 -9
View File
@@ -113,10 +113,6 @@ pub struct QueueManager<R: tauri::Runtime = tauri::Wry> {
/// Download ids whose aria2 retry loop must not create another job.
aria2_retry_cancelled: Mutex<HashSet<String>>,
/// Serializes retry addUri with remove so a late retry cannot escape
/// cancellation and continue writing after deletion.
aria2_retry_add_lock: Mutex<()>,
spawner: Arc<dyn SidecarSpawner>,
app_handle: AppHandle<R>,
}
@@ -152,7 +148,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
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<R: tauri::Runtime> QueueManager<R> {
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<String> {
self.aria2_gids
@@ -642,7 +635,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
return;
}
let _retry_add_guard = this.aria2_retry_add_lock.lock().await;
if !this.active_permits.lock().await.contains_key(&id_for_task) {
return;
}
+1 -1
View File
@@ -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",
];
+5 -2
View File
@@ -114,7 +114,8 @@ export const getSiteLogin = (url: string, settings: ReturnType<typeof useSetting
const suffix = pattern.substring(2);
if (host === suffix || host.endsWith('.' + suffix)) return login;
} else if (pattern.includes('*')) {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
const collapsed = pattern.replace(/\*+/g, '*');
const escaped = collapsed.replace(/[.+?^${}()|[\]\\]/g, '\\$&');
const regex = new RegExp('^' + escaped.replace(/\*/g, '.*') + '$');
if (regex.test(host)) return login;
} else if (host === pattern) {
@@ -242,6 +243,7 @@ export const useDownloadStore = create<DownloadState>((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<DownloadState>((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) => {