fix: harden audited download and release paths

This commit is contained in:
NimBold
2026-07-21 08:39:54 +03:30
parent 69ce2b15ba
commit 886388d5f2
14 changed files with 219 additions and 51 deletions
+13 -3
View File
@@ -443,7 +443,7 @@ fn normalize_download(mut payload: ExtensionRequest) -> Option<ExtensionDownload
(!value.is_empty() && value.chars().count() <= 512).then_some(value)
});
// A multi-URL handoff has no per-URL cookie scope. Keep ordinary
// request headers, but drop Cookie headers and the dedicated cookie field
// request headers, but drop credential-bearing headers and the dedicated cookie field
// so a legacy or untrusted caller cannot reuse one session across hosts.
let headers = normalize_headers(payload.headers, payload.media || urls.len() > 1);
let cookie_scopes = if !payload.media && urls.len() == 1 {
@@ -548,7 +548,17 @@ fn normalize_headers(headers: Option<String>, media: bool) -> Option<String> {
.lines()
.filter(|line| {
line.split_once(':')
.map(|(name, _)| !name.trim().eq_ignore_ascii_case("cookie"))
.map(|(name, _)| {
!matches!(
name.trim().to_ascii_lowercase().as_str(),
"authorization"
| "cookie"
| "cookie2"
| "proxy-authorization"
| "set-cookie"
| "set-cookie2"
)
})
.unwrap_or(true)
})
.collect::<Vec<_>>()
@@ -881,7 +891,7 @@ mod tests {
silent: false,
filename: None,
headers: Some(format!(
"Cookie: stale={};\nUser-Agent: Firefox",
"Cookie: stale={};\nCookie2: stale=1\nAuthorization: Bearer stale\nProxy-Authorization: Basic stale\nSet-Cookie: stale=1\nSet-Cookie2: stale=1\nUser-Agent: Firefox",
"x".repeat(64 * 1024)
)),
cookies: Some(format!("large={}", "x".repeat(64 * 1024))),
+8 -1
View File
@@ -1912,6 +1912,8 @@ async fn fetch_metadata(
let mut current_res = match head_req.send().await {
Ok(response) => response,
Err(head_error) => build_get_range().send().await.map_err(|get_error| {
let head_error = crate::redact_sensitive_text(&head_error.to_string());
let get_error = crate::redact_sensitive_text(&get_error.to_string());
format!(
"HEAD metadata request failed ({head_error}); ranged GET fallback failed ({get_error})"
)
@@ -2815,6 +2817,9 @@ fn approve_download_root(app_handle: tauri::AppHandle, path: String) -> Result<S
if !resolved.is_absolute() {
return Err("Download root must be an absolute path".to_string());
}
if crate::path_has_symlink_component(&resolved) {
return Err("Download root may not contain symlink components".to_string());
}
let canonical = std::fs::canonicalize(&resolved)
.map_err(|error| format!("Failed to resolve download root: {error}"))?;
if !canonical.is_dir() {
@@ -3892,7 +3897,9 @@ pub(crate) async fn start_media_download_internal(
// user-installed tools, or platform-specific executable aliases.
let trusted_path = crate::platform::trusted_system_path()?;
let max_retries = max_tries.unwrap_or(0).max(0) as usize;
let max_retries = max_tries
.unwrap_or(crate::retry::MAX_RETRIES as i32)
.max(0) as usize;
let mut strike = 0_usize;
let mut effective_cookie_source = cookie_source.clone();
let mut browser_cookie_fallback_used = false;
+12 -4
View File
@@ -1,5 +1,5 @@
use crate::ipc::{DownloadStateEvent, DownloadStatus, QueueDirection};
use crate::retry::{backoff_and_emit, is_transient_network_error, BackoffOutcome};
use crate::retry::{backoff_and_emit, is_transient_network_error, BackoffOutcome, MAX_RETRIES};
use log;
use serde::Deserialize;
use serde_json;
@@ -1600,7 +1600,7 @@ impl<R: tauri::Runtime> QueueManager<R> {
}
fn automatic_retry_limit(max_tries: Option<i32>) -> usize {
max_tries.unwrap_or(0).max(0) as usize
max_tries.unwrap_or(MAX_RETRIES as i32).max(0) as usize
}
fn aria2_attempt_limit(max_tries: Option<i32>) -> u32 {
@@ -1977,8 +1977,9 @@ impl SidecarSpawner for ProductionSpawner {
}
}
Err(e) => {
log::error!("aria2 addUri [{}] failed: {}", id, e);
Err(format!("aria2 addUri failed: {e}"))
let safe_error = crate::redact_sensitive_text(&e);
log::error!("aria2 addUri [{}] failed: {}", id, safe_error);
Err(format!("aria2 addUri failed: {safe_error}"))
}
}
}
@@ -2288,4 +2289,11 @@ mod tests {
assert_eq!(aria2_attempt_limit(Some(0)), 1);
assert_eq!(aria2_attempt_limit(Some(10)), 1);
}
#[test]
fn omitted_retry_limit_uses_the_shared_default_but_zero_stays_explicit() {
assert_eq!(automatic_retry_limit(None), MAX_RETRIES);
assert_eq!(automatic_retry_limit(Some(0)), 0);
assert_eq!(automatic_retry_limit(Some(2)), 2);
}
}
+95 -12
View File
@@ -26,6 +26,26 @@ fn stop_is_due(
&& last_stop_key != stop_key
}
fn overnight_stop_is_due(
stop_time_enabled: bool,
start_minute: Option<u32>,
stop_minute: Option<u32>,
current_minute: u32,
previous_day_allowed: bool,
last_start_key: &str,
previous_start_key: &str,
last_stop_key: &str,
stop_key: &str,
) -> bool {
stop_time_enabled
&& previous_day_allowed
&& start_minute.zip(stop_minute).is_some_and(|(start, stop)| {
stop < start && current_minute >= stop && current_minute < start
})
&& last_start_key == previous_start_key
&& last_stop_key != stop_key
}
pub fn spawn_scheduler(
app_handle: tauri::AppHandle,
settings_cache: Arc<RwLock<Option<crate::ipc::PersistedSettings>>>,
@@ -56,20 +76,20 @@ pub fn spawn_scheduler(
let allowed_today =
scheduler.everyday || scheduler.selected_days.contains(&current_day);
if !allowed_today {
continue;
}
let date_key = now.format("%Y-%m-%d").to_string();
let start_key = format!("{date_key}-start");
let stop_key = format!("{date_key}-stop");
let start_minute = minute_of_day(&scheduler.start_time);
let stop_minute = minute_of_day(&scheduler.stop_time);
let overnight = start_minute
.zip(stop_minute)
.is_some_and(|(start, stop)| stop < start);
let before_stop = !scheduler.stop_time_enabled
|| stop_minute.is_some_and(|stop| current_minute < stop);
if start_minute.is_some_and(|start| current_minute >= start)
&& before_stop
if allowed_today
&& start_minute.is_some_and(|start| current_minute >= start)
&& (overnight || before_stop)
&& scheduler_last_start_key != start_key
&& last_emit
.get("start")
@@ -85,17 +105,43 @@ pub fn spawn_scheduler(
last_emit.insert("start", std::time::Instant::now());
}
if stop_is_due(
let same_day_stop_due = allowed_today
&& !overnight
&& stop_is_due(
scheduler.stop_time_enabled,
stop_minute,
current_minute,
&scheduler_last_start_key,
&start_key,
&scheduler_last_stop_key,
&stop_key,
);
let previous_day = now.date_naive().pred_opt();
let previous_day_allowed = previous_day.is_some_and(|day| {
scheduler.everyday
|| scheduler
.selected_days
.contains(&day.weekday().num_days_from_sunday())
});
let previous_start_key = previous_day
.map(|day| format!("{}-start", day.format("%Y-%m-%d")))
.unwrap_or_default();
let overnight_stop_due = overnight_stop_is_due(
scheduler.stop_time_enabled,
start_minute,
stop_minute,
current_minute,
previous_day_allowed,
&scheduler_last_start_key,
&start_key,
&previous_start_key,
&scheduler_last_stop_key,
&stop_key,
) && last_emit
.get("stop")
.is_none_or(|instant| instant.elapsed() >= Duration::from_secs(5))
);
if (same_day_stop_due || overnight_stop_due)
&& last_emit
.get("stop")
.is_none_or(|instant| instant.elapsed() >= Duration::from_secs(5))
{
let _ = app_handle.emit(
"schedule-trigger",
@@ -113,7 +159,7 @@ pub fn spawn_scheduler(
#[cfg(test)]
mod tests {
use super::{minute_of_day, stop_is_due};
use super::{minute_of_day, overnight_stop_is_due, stop_is_due};
#[test]
fn parses_valid_scheduler_times() {
@@ -150,4 +196,41 @@ mod tests {
"2026-06-22-stop",
));
}
#[test]
fn overnight_stop_uses_the_previous_day_start() {
assert!(overnight_stop_is_due(
true,
Some(1320),
Some(360),
420,
true,
"2026-06-22-start",
"2026-06-22-start",
"",
"2026-06-23-stop",
));
assert!(!overnight_stop_is_due(
true,
Some(1320),
Some(360),
1380,
true,
"2026-06-22-start",
"2026-06-22-start",
"",
"2026-06-22-stop",
));
assert!(!overnight_stop_is_due(
true,
Some(1320),
Some(360),
420,
false,
"2026-06-22-start",
"2026-06-22-start",
"",
"2026-06-23-stop",
));
}
}