fix: resolve P0 and P1 audit findings

- Fix state corruption on deleted queues in frontend
- Prevent concurrent dispatch of duplicate downloads
- Fix file paths when deleting native downloads
- Ensure pausing and resuming items persist state to database
- Remove duplicate IPC invocations for speed limits
- Stop PropertiesModal from resetting inputs during download progress
- Switch aria2 secret to use secure v4 UUID
- Fix SSRF vulnerability in fetch_metadata via DNS rebinding block
- Resolve race condition when reading media download logs
- Gracefully handle panics when acquiring prevent sleep lock
- Rate limit native downloads respecting global settings
- Enforce TLS certificate validation in backend requests
This commit is contained in:
NimBold
2026-06-15 14:10:45 +03:30
parent 22d59e3c11
commit 285ec0a81d
6 changed files with 132 additions and 93 deletions
+1 -3
View File
@@ -98,9 +98,7 @@ export const DownloadTable: React.FC<DownloadTableProps> = ({ filter }) => {
};
const handleResume = (item: DownloadItem) => {
useDownloadStore.setState((state) => ({
downloads: state.downloads.map(d => d.id === item.id ? { ...d, status: 'queued', speed: '-', eta: '-' } : d)
}));
updateDownload(item.id, { status: 'queued', _dispatched: false, speed: '-', eta: '-' });
useDownloadStore.getState().processQueue();
};
+2 -3
View File
@@ -10,7 +10,6 @@ export const PropertiesModal = () => {
const {
selectedPropertiesDownloadId,
setSelectedPropertiesDownloadId,
downloads,
updateDownload
} = useDownloadStore();
@@ -43,7 +42,7 @@ export const PropertiesModal = () => {
useEffect(() => {
if (selectedPropertiesDownloadId) {
const activeItem = downloads.find(d => d.id === selectedPropertiesDownloadId);
const activeItem = useDownloadStore.getState().downloads.find(d => d.id === selectedPropertiesDownloadId);
if (activeItem) {
setItem(activeItem);
setUrl(activeItem.url);
@@ -89,7 +88,7 @@ export const PropertiesModal = () => {
} else {
setItem(null);
}
}, [selectedPropertiesDownloadId, downloads, defaultDownloadPath]);
}, [selectedPropertiesDownloadId, defaultDownloadPath]);
if (!selectedPropertiesDownloadId || !item) return null;
+103 -66
View File
@@ -87,6 +87,8 @@ interface DownloadState {
initDB: () => Promise<void>;
}
let isProcessingQueue = false;
export const useDownloadStore = create<DownloadState>((set, get) => ({
downloads: [],
queues: [{ id: MAIN_QUEUE_ID, name: 'Main Queue', isMain: true }],
@@ -167,13 +169,15 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
const item = get().downloads.find(d => d.id === id);
if (item && item.status === 'downloading') {
try {
await invoke('remove_download', { id, filepath: item.destination || null });
const filepath = item.destination ? `${item.destination}/${item.fileName}` : null;
await invoke('remove_download', { id, filepath });
} catch (e) {
console.error("Failed to terminate download on deletion:", e);
}
} else if (item && deleteFile) {
try {
await invoke('remove_download', { id, filepath: item.destination || null });
const filepath = item.destination ? `${item.destination}/${item.fileName}` : null;
await invoke('remove_download', { id, filepath });
} catch (e) {
console.error("Failed to delete file from disk:", e);
}
@@ -218,6 +222,14 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
: item
)
}));
const downloadsToSave = get().downloads.filter(item => runnableIds.includes(item.id));
downloadsToSave.forEach(d => {
const toSave = { ...d };
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
invoke('db_save_download', { id: toSave.id, status: toSave.status, queueId: toSave.queueId, data: JSON.stringify(toSave) }).catch(console.error);
});
await get().processQueue();
return runnableIds.length;
},
@@ -235,6 +247,14 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
: item
)
}));
const downloadsToSave = get().downloads.filter(item => activeIds.includes(item.id));
downloadsToSave.forEach(d => {
const toSave = { ...d };
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
invoke('db_save_download', { id: toSave.id, status: toSave.status, queueId: toSave.queueId, data: JSON.stringify(toSave) }).catch(console.error);
});
await Promise.all(activeIds.map(id => invoke('pause_download', { id }).catch(() => {})));
syncSystemIntegrations();
return activeIds.length;
@@ -265,6 +285,9 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
},
removeQueue: (id) => {
if (id === MAIN_QUEUE_ID) return;
const affectedDownloads = get().downloads.filter(d => d.queueId === id);
set((state) => ({
queues: state.queues.filter(q => q.id !== id),
downloads: state.downloads.map(d =>
@@ -274,8 +297,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
invoke('db_delete_queue', { id }).catch(console.error);
// Also we need to save the updated downloads to DB
const downloads = get().downloads.filter(d => d.queueId === id);
downloads.forEach(d => {
affectedDownloads.forEach(d => {
const toSave = { ...d, queueId: MAIN_QUEUE_ID };
delete toSave.fraction; delete toSave.speed; delete toSave.eta;
invoke('db_save_download', { id: toSave.id, status: toSave.status, queueId: MAIN_QUEUE_ID, data: JSON.stringify(toSave) }).catch(console.error);
@@ -296,6 +318,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
// Auto resume downloads that were active
const active = get().downloads.filter(d => d.status === 'downloading');
const settings = useSettingsStore.getState();
active.forEach(item => {
if (item.isMedia) {
invoke('start_media_download', {
@@ -305,7 +328,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
filename: item.fileName,
formatSelector: item.mediaFormatSelector || null,
cookieSource: null,
speedLimit: item.speedLimit || null,
speedLimit: item.speedLimit || settings.globalSpeedLimit || null,
username: item.username || null,
password: item.password || null,
headers: item.headers || null,
@@ -320,7 +343,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
destination: item.destination || '~/Downloads',
filename: item.fileName,
connections: item.connections ?? null,
speedLimit: item.speedLimit || null,
speedLimit: item.speedLimit || settings.globalSpeedLimit || null,
username: item.username || null,
password: item.password || null,
headers: item.headers || null,
@@ -340,70 +363,84 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
}
},
processQueue: async () => {
const { downloads, updateDownload } = get();
if (isProcessingQueue) return;
isProcessingQueue = true;
// Find all queued items that haven't been dispatched to the backend yet
const itemsToStart = downloads.filter(d => d.status === 'queued' && !d._dispatched);
for (const item of itemsToStart) {
// Mark as dispatched so we don't send it again on the next pass
updateDownload(item.id, { _dispatched: true });
try {
const settings = useSettingsStore.getState();
const login = getSiteLogin(item.url, settings);
let keychainPassword = null;
if (login) {
try {
keychainPassword = await invoke('get_keychain_password', { id: login.id });
} catch (e) {
console.warn("Could not fetch keychain password for login:", e);
}
}
const destPath = item.destination ||
(settings.downloadDirectories && settings.downloadDirectories[item.category]) ||
settings.defaultDownloadPath ||
'~/Downloads';
try {
const { downloads, updateDownload } = get();
const settings = useSettingsStore.getState();
const concurrentLimit = settings.maxConcurrentDownloads || 3;
const activeCount = downloads.filter(d => d.status === 'downloading').length;
let availableSlots = concurrentLimit - activeCount;
if (item.isMedia) {
await invoke('start_media_download', {
id: item.id,
url: item.url,
destination: destPath,
filename: item.fileName,
formatSelector: item.mediaFormatSelector || null,
cookieSource: settings.mediaCookieSource !== 'none' ? settings.mediaCookieSource : null,
speedLimit: item.speedLimit || null,
username: item.username || (login ? login.username : null),
password: item.password || keychainPassword,
headers: item.headers || null,
proxy: getProxyArgs(settings),
userAgent: settings.customUserAgent || null,
maxTries: settings.maxAutomaticRetries
});
} else {
await invoke('start_download', {
id: item.id,
url: item.url,
destination: destPath,
filename: item.fileName,
connections: item.connections || settings.perServerConnections || null,
speedLimit: item.speedLimit || null,
username: item.username || (login ? login.username : null),
password: item.password || keychainPassword,
headers: item.headers || null,
checksum: item.checksum || null,
cookies: item.cookies || null,
mirrors: item.mirrors || null,
userAgent: settings.customUserAgent || null,
maxTries: settings.maxAutomaticRetries,
proxy: getProxyArgs(settings)
});
if (availableSlots <= 0) return;
const itemsToStart = downloads.filter(d => d.status === 'queued' && !d._dispatched);
for (const item of itemsToStart) {
if (availableSlots <= 0) break;
availableSlots--;
// Mark as dispatched so we don't send it again on the next pass
updateDownload(item.id, { _dispatched: true });
try {
const login = getSiteLogin(item.url, settings);
let keychainPassword = null;
if (login) {
try {
keychainPassword = await invoke('get_keychain_password', { id: login.id });
} catch (e) {
console.warn("Could not fetch keychain password for login:", e);
}
}
const destPath = item.destination ||
(settings.downloadDirectories && settings.downloadDirectories[item.category]) ||
settings.defaultDownloadPath ||
'~/Downloads';
if (item.isMedia) {
await invoke('start_media_download', {
id: item.id,
url: item.url,
destination: destPath,
filename: item.fileName,
formatSelector: item.mediaFormatSelector || null,
cookieSource: settings.mediaCookieSource !== 'none' ? settings.mediaCookieSource : null,
speedLimit: item.speedLimit || settings.globalSpeedLimit || null,
username: item.username || (login ? login.username : null),
password: item.password || keychainPassword,
headers: item.headers || null,
proxy: getProxyArgs(settings),
userAgent: settings.customUserAgent || null,
maxTries: settings.maxAutomaticRetries
});
} else {
await invoke('start_download', {
id: item.id,
url: item.url,
destination: destPath,
filename: item.fileName,
connections: item.connections || settings.perServerConnections || null,
speedLimit: item.speedLimit || settings.globalSpeedLimit || null,
username: item.username || (login ? login.username : null),
password: item.password || keychainPassword,
headers: item.headers || null,
checksum: item.checksum || null,
cookies: item.cookies || null,
mirrors: item.mirrors || null,
userAgent: settings.customUserAgent || null,
maxTries: settings.maxAutomaticRetries,
proxy: getProxyArgs(settings)
});
}
} catch (e) {
console.error("Failed to start queued download:", e);
updateDownload(item.id, { status: 'failed' });
}
} catch (e) {
console.error("Failed to start queued download:", e);
updateDownload(item.id, { status: 'failed' });
}
} finally {
isProcessingQueue = false;
}
}
}));
-2
View File
@@ -233,11 +233,9 @@ export const useSettingsStore = create<SettingsState>()(
setDefaultDownloadPath: (path) => set({ defaultDownloadPath: path }),
setMaxConcurrentDownloads: (max) => {
set({ maxConcurrentDownloads: max });
invoke('set_concurrent_limit', { limit: max }).catch(console.error);
},
setGlobalSpeedLimit: (limit) => {
set({ globalSpeedLimit: limit });
invoke('set_global_speed_limit', { limit: limit === '' || limit === '0' ? null : limit }).catch(console.error);
},
setActiveView: (view) => set({ activeView: view }),
setActiveSettingsTab: (activeSettingsTab) => set({ activeSettingsTab }),