mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
fix: resolve 7 critical security vulnerabilities from audit
This commit is contained in:
Generated
+10
-57
@@ -13,6 +13,7 @@
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||
"@tauri-apps/plugin-log": "^2.8.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
@@ -227,9 +228,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -246,9 +244,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -265,9 +260,6 @@
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -284,9 +276,6 @@
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -303,9 +292,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -322,9 +308,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -538,9 +521,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -557,9 +537,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -576,9 +553,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -595,9 +569,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -781,9 +752,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -801,9 +769,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -821,9 +786,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -841,9 +803,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -861,9 +820,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "Apache-2.0 OR MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -942,6 +898,15 @@
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-fs": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.1.tgz",
|
||||
"integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-log": {
|
||||
"version": "2.8.0",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz",
|
||||
@@ -1564,9 +1529,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1587,9 +1549,6 @@
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1610,9 +1569,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -1633,9 +1589,6 @@
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
"@tauri-apps/api": "^2.11.1",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
"@tauri-apps/plugin-dialog": "^2.7.1",
|
||||
"@tauri-apps/plugin-fs": "^2.5.1",
|
||||
"@tauri-apps/plugin-log": "^2.8.0",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2",
|
||||
|
||||
+53
-29
@@ -717,6 +717,39 @@ async fn cleanup_media_artifacts(out_path: &std::path::Path, remove_primary: boo
|
||||
|
||||
|
||||
|
||||
async fn validate_url_ssrf(url: &str) -> Result<Option<(String, std::net::SocketAddr)>, String> {
|
||||
let parsed = reqwest::Url::parse(url).map_err(|_| "SSRF blocked: Invalid URL")?;
|
||||
if parsed.scheme() != "http" && parsed.scheme() != "https" {
|
||||
return Err("SSRF blocked: Only HTTP/HTTPS schemes allowed".to_string());
|
||||
}
|
||||
let host = parsed.host_str().ok_or("SSRF blocked: No host")?;
|
||||
let port = parsed.port_or_known_default().unwrap_or(80);
|
||||
|
||||
let mut addrs = tokio::net::lookup_host((host, port))
|
||||
.await
|
||||
.map_err(|_| "SSRF blocked: DNS resolution failed")?;
|
||||
|
||||
let addr = addrs.next().ok_or("SSRF blocked: No DNS records")?;
|
||||
let ip = addr.ip();
|
||||
|
||||
if ip.is_loopback() || ip.is_multicast() || ip.is_unspecified() {
|
||||
return Err("SSRF blocked: Private/local IP not allowed".to_string());
|
||||
}
|
||||
match ip {
|
||||
std::net::IpAddr::V4(ipv4) => {
|
||||
if ipv4.is_private() || ipv4.is_link_local() {
|
||||
return Err("SSRF blocked: Private/local IP not allowed".to_string());
|
||||
}
|
||||
}
|
||||
std::net::IpAddr::V6(ipv6) => {
|
||||
if (ipv6.segments()[0] & 0xfe00) == 0xfc00 { // ULA check
|
||||
return Err("SSRF blocked: Private/local IP not allowed".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some((host.to_string(), addr)))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn fetch_metadata(url: String, user_agent: Option<String>, username: Option<String>, password: Option<String>) -> Result<MetadataResponse, String> {
|
||||
let mut current_url = url.clone();
|
||||
@@ -740,26 +773,7 @@ async fn fetch_metadata(url: String, user_agent: Option<String>, username: Optio
|
||||
builder = builder.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
|
||||
}
|
||||
|
||||
let mut resolved_addr = None;
|
||||
if let Ok(parsed) = reqwest::Url::parse(¤t_url) {
|
||||
if let Some(host) = parsed.host_str() {
|
||||
let port = parsed.port_or_known_default().unwrap_or(80);
|
||||
if let Ok(addrs) = std::net::ToSocketAddrs::to_socket_addrs(&(host, port)) {
|
||||
if let Some(addr) = addrs.into_iter().next() {
|
||||
let ip = addr.ip();
|
||||
if ip.is_loopback() || ip.is_multicast() || ip.is_unspecified() {
|
||||
return Err("SSRF blocked: Private/local IP not allowed".to_string());
|
||||
}
|
||||
if let std::net::IpAddr::V4(ipv4) = ip {
|
||||
if ipv4.is_private() || ipv4.is_link_local() {
|
||||
return Err("SSRF blocked: Private/local IP not allowed".to_string());
|
||||
}
|
||||
}
|
||||
resolved_addr = Some((host.to_string(), addr));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let resolved_addr = validate_url_ssrf(¤t_url).await?;
|
||||
|
||||
if let Some((host, addr)) = resolved_addr {
|
||||
builder = builder.resolve(&host, addr);
|
||||
@@ -909,6 +923,7 @@ async fn fetch_media_metadata(
|
||||
username: Option<String>,
|
||||
password: Option<String>,
|
||||
) -> Result<MediaMetadata, String> {
|
||||
validate_url_ssrf(&url).await?;
|
||||
let cache_key = media_metadata_cache_key(&url, &cookie_browser, &username, &password);
|
||||
|
||||
let cache = MEDIA_METADATA_CACHE.get_or_init(|| tokio::sync::Mutex::new(HashMap::new()));
|
||||
@@ -1011,12 +1026,14 @@ async fn fetch_media_metadata_uncached(app_handle: tauri::AppHandle, url: String
|
||||
let mut config_content = String::new();
|
||||
if let Some(user) = username {
|
||||
if !user.is_empty() {
|
||||
config_content.push_str(&format!("--username\n{}\n", user));
|
||||
let safe_user = user.replace('\n', "").replace('\r', "");
|
||||
config_content.push_str(&format!("--username\n{}\n", safe_user));
|
||||
}
|
||||
}
|
||||
if let Some(pass) = password {
|
||||
if !pass.is_empty() {
|
||||
config_content.push_str(&format!("--password\n{}\n", pass));
|
||||
let safe_pass = pass.replace('\n', "").replace('\r', "");
|
||||
config_content.push_str(&format!("--password\n{}\n", safe_pass));
|
||||
}
|
||||
}
|
||||
use std::io::Write;
|
||||
@@ -1246,6 +1263,7 @@ struct Aria2DaemonGuard {
|
||||
child: Mutex<Option<std::process::Child>>,
|
||||
startup_error: Mutex<Option<String>>,
|
||||
last_stderr: Mutex<String>,
|
||||
config_path: Mutex<Option<tempfile::TempPath>>,
|
||||
}
|
||||
|
||||
impl Aria2DaemonGuard {
|
||||
@@ -1254,6 +1272,7 @@ impl Aria2DaemonGuard {
|
||||
child: Mutex::new(None),
|
||||
startup_error: Mutex::new(None),
|
||||
last_stderr: Mutex::new(String::new()),
|
||||
config_path: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3303,7 +3322,6 @@ async fn clear_logs(app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
async fn export_logs(
|
||||
app_handle: tauri::AppHandle,
|
||||
state: tauri::State<'_, AppState>,
|
||||
dest_path: String,
|
||||
) -> Result<String, String> {
|
||||
let mut output = format!(
|
||||
"Firelink support logs\nVersion: {}\nOS: {} {}\nArchitecture: {}\nGenerated: {}\n\n",
|
||||
@@ -3360,10 +3378,7 @@ async fn export_logs(
|
||||
}
|
||||
output.push('\n');
|
||||
}
|
||||
tokio::fs::write(&dest_path, output)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(dest_path)
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -4004,9 +4019,16 @@ pub fn run() {
|
||||
match resolve_bundled_binary_path(app.handle(), "aria2c") {
|
||||
Ok(binary_path) => {
|
||||
let mut cmd = std::process::Command::new(&binary_path);
|
||||
|
||||
let mut config_file = tempfile::Builder::new().prefix("aria2-").suffix(".conf").tempfile().expect("failed to create aria2 config file");
|
||||
use std::io::Write;
|
||||
let config_content = format!("rpc-secret={}\n", aria2_secret);
|
||||
config_file.write_all(config_content.as_bytes()).expect("failed to write aria2 config file");
|
||||
let config_path = config_file.into_temp_path();
|
||||
|
||||
cmd.arg("--enable-rpc=true")
|
||||
.arg(format!("--conf-path={}", config_path.display()))
|
||||
.arg(format!("--rpc-listen-port={}", aria2_port))
|
||||
.arg(format!("--rpc-secret={}", aria2_secret))
|
||||
.arg("--rpc-listen-all=false")
|
||||
.arg("--continue=true")
|
||||
.arg("--retry-wait=2")
|
||||
@@ -4053,6 +4075,7 @@ pub fn run() {
|
||||
|
||||
let guard = app.state::<Aria2DaemonGuard>();
|
||||
*guard.child.lock().unwrap() = Some(child);
|
||||
*guard.config_path.lock().unwrap() = Some(config_path);
|
||||
|
||||
let port = aria2_port;
|
||||
let secret = aria2_secret.clone();
|
||||
@@ -4141,7 +4164,8 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Connection lost, loop and reconnect
|
||||
// Connection lost, clear permits and reconnect
|
||||
app_handle_ws.state::<AppState>().queue_manager.clear_aria2_permits().await;
|
||||
tokio::time::sleep(std::time::Duration::from_secs(4)).await;
|
||||
}
|
||||
});
|
||||
|
||||
+16
-2
@@ -240,8 +240,6 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
true
|
||||
}
|
||||
|
||||
/// Release the permit parked under `id`, if any. Idempotent. Wakes the
|
||||
/// dispatcher so a freed slot is claimed promptly.
|
||||
pub async fn release_permit(&self, id: &str) {
|
||||
let removed = self.active_permits.lock().await.remove(id).is_some();
|
||||
self.active_kinds.lock().await.remove(id);
|
||||
@@ -250,6 +248,22 @@ impl<R: tauri::Runtime> QueueManager<R> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all permits belonging to aria2. Useful when aria2 WS connection drops.
|
||||
pub async fn clear_aria2_permits(&self) {
|
||||
let ids_to_fail: Vec<String> = {
|
||||
let kinds = self.active_kinds.lock().await;
|
||||
kinds
|
||||
.iter()
|
||||
.filter(|(_, kind)| matches!(kind, TaskKind::Aria2))
|
||||
.map(|(id, _)| id.clone())
|
||||
.collect()
|
||||
};
|
||||
|
||||
for id in ids_to_fail {
|
||||
self.apply_completion(&id, PendingOutcome::Error("Aria2 WebSocket connection lost".to_string())).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Number of un-acquired permits currently in the semaphore pool.
|
||||
pub fn available_permits(&self) -> usize {
|
||||
self.semaphore.available_permits()
|
||||
|
||||
+11
-2
@@ -368,6 +368,8 @@ function App() {
|
||||
const allCompleted = scheduledItems.every(item => item?.status === 'completed');
|
||||
settings.setSchedulerActiveDownloadIds([]);
|
||||
settings.setSchedulerRunning(false);
|
||||
|
||||
let timer: number | undefined;
|
||||
if (!allCompleted) {
|
||||
addToast({
|
||||
message: 'Scheduled downloads did not all complete. The post-queue system action was skipped.',
|
||||
@@ -395,7 +397,7 @@ function App() {
|
||||
</div>
|
||||
)
|
||||
});
|
||||
window.setTimeout(() => {
|
||||
timer = window.setTimeout(() => {
|
||||
if (cancelled) return;
|
||||
const activeTransfers = useDownloadStore.getState().downloads.some(download =>
|
||||
isActiveDownloadStatus(download.status)
|
||||
@@ -418,6 +420,12 @@ function App() {
|
||||
});
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (timer !== undefined) {
|
||||
window.clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
}, [addToast, downloads, schedulerRunning, schedulerActiveDownloadIds]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -505,7 +513,7 @@ function App() {
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
initDownloadListener();
|
||||
const unlistenDownload = initDownloadListener();
|
||||
|
||||
const unlistenTerminalState = listen('download-state', (event) => {
|
||||
if (event.payload.status !== 'completed' && event.payload.status !== 'failed') return;
|
||||
@@ -561,6 +569,7 @@ function App() {
|
||||
unlistenTerminalState.then(f => f());
|
||||
unlistenExtension.then(f => f());
|
||||
unlistenDeepLink.then(f => f());
|
||||
unlistenDownload.then(f => { if (f) f(); });
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import { writeTextFile } from '@tauri-apps/plugin-fs';
|
||||
import { attachLogger, setLogPaused, getLogPaused, initLogger } from '../utils/logger';
|
||||
import { FileDown, Trash2, Terminal, Filter, Play, Pause, Info, Copy } from 'lucide-react';
|
||||
import { WindowDragRegion } from './WindowDragRegion';
|
||||
@@ -128,7 +129,8 @@ export default function LogsView() {
|
||||
filters: [{ name: 'Log Files', extensions: ['log'] }],
|
||||
});
|
||||
if (!path) return;
|
||||
await invoke('export_logs', { destPath: path });
|
||||
const logsContent = await invoke('export_logs', {});
|
||||
await writeTextFile(path, logsContent);
|
||||
addToast({ message: 'Support logs exported', variant: 'success' });
|
||||
} catch (e) {
|
||||
console.error('Export failed:', e);
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ type CommandMap = {
|
||||
args: { baseFolder: string; subfolders: Record<string, string> };
|
||||
result: void;
|
||||
};
|
||||
export_logs: { args: { destPath: string }; result: string };
|
||||
export_logs: { args: Record<string, never>; result: string };
|
||||
read_logs: { args: { limit: number }; result: string[] };
|
||||
clear_logs: { args: undefined; result: void };
|
||||
toggle_log_pause: { args: { pause: boolean }; result: void };
|
||||
|
||||
@@ -98,4 +98,19 @@ export async function initDownloadListener() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (unlistenProgress) {
|
||||
unlistenProgress();
|
||||
unlistenProgress = null;
|
||||
}
|
||||
if (unlistenState) {
|
||||
unlistenState();
|
||||
unlistenState = null;
|
||||
}
|
||||
if (unlistenTray) {
|
||||
unlistenTray();
|
||||
unlistenTray = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user