mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix: harden audited download and release paths
This commit is contained in:
@@ -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))),
|
||||
|
||||
@@ -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
@@ -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
@@ -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(¤t_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",
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user