From a7b8cd52af3091b85a104a9acb0bd567fb3e7b59 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Wed, 12 Aug 2026 20:28:27 +0800 Subject: [PATCH] =?UTF-8?q?fix(=E5=AE=89=E5=85=A8/=E6=80=A7=E8=83=BD):=20A?= =?UTF-8?q?I=E5=AE=A1=E6=9F=A5=E5=AE=9E=E8=AF=81=E4=BF=AE=E5=A4=8D=20-=20m?= =?UTF-8?q?etalinkUrl=20SSRF=E6=A0=A1=E9=AA=8C=20+=20withTimeout=E5=AE=9A?= =?UTF-8?q?=E6=97=B6=E5=99=A8=E6=B3=84=E6=BC=8F=20+=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E6=9B=B4=E6=96=B0/=E5=88=9D=E5=A7=8B=E5=8A=A0=E8=BD=BD?= =?UTF-8?q?=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - install-manager.ts: queue-install 增加 metalinkUrl 校验(仅允许相对路径或官方域名,防 SSRF) - App.vue: withTimeout 用 finally(clearTimeout) 修复定时器泄漏 - App.vue: apps 批量更新改数组赋值替代 push(...),避免逐元素响应式重渲染 - App.vue: apps.length watcher 增加 initialCatalogLoaded 守卫,初始目录加载期间跳过已安装列表刷新 IPC - 审计实证:①filename校验已有(误报) ④pauseDownload为历史TODO且后端不支持(不在此PR) ⑦⑧为可维护性大项(记录待后续) 注:AI审查4阻断项实证——①误报 ②真实加固 ③真实修复 ④历史遗留;4改进项 ⑤⑥真实已修 ⑦⑧记录 --- electron/main/backend/install-manager.ts | 14 ++++++++++++++ src/App.vue | 21 ++++++++++++++------- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index c345720b..8100cd62 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -313,6 +313,20 @@ ipcMain.on("queue-install", async (event, download_json) => { return; } + // metalinkUrl 来自渲染端(由目录 filename / 主进程解析的 downloadUrl 拼成)。 + // 纵深防御:仅允许相对路径(拼接 baseURL)或以官方域名开头的绝对 URL, + // 拒绝任意外部地址,防止 axios 忽略 baseURL 发起 SSRF/任意源下载。 + if (metalinkUrl) { + const isRelative = metalinkUrl.startsWith("/"); + const isOfficial = metalinkUrl.startsWith( + "https://erotica.spark-app.store", + ); + if (!isRelative && !isOfficial) { + logger.warn(`queue-install invalid metalinkUrl: ${metalinkUrl}`); + return; + } + } + logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}, 来源: ${origin}`); const webContents = event.sender; diff --git a/src/App.vue b/src/App.vue index 01fbe0e5..b1123a46 100644 --- a/src/App.vue +++ b/src/App.vue @@ -509,17 +509,17 @@ const isInstalledAppInfo = (value: unknown): value is InstalledAppInfo => { const LIST_INSTALLED_TIMEOUT_MS = 15000; // 为 Promise 增加超时控制:超时即 reject,配合 Promise.allSettled 让单来源失败不影响其它来源。 +// 注意:promise 先 resolve 时必须 clearTimeout,否则 setTimeout 句柄泄漏。 const withTimeout = ( promise: Promise, ms: number, label: string, ): Promise => { - return Promise.race([ - promise, - new Promise((_, reject) => - setTimeout(() => reject(new Error(`${label} 超时(${ms}ms)`)), ms), - ), - ]); + let timer: ReturnType; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error(`${label} 超时(${ms}ms)`)), ms); + }); + return Promise.race([promise.finally(() => clearTimeout(timer)), timeout]); }; // 响应式状态 @@ -1946,10 +1946,14 @@ const refreshInstalledApps = async () => { // 现改为 len 任意 >0 的正向变化都允许触发;密集分批推送由下方 300ms 防抖合并最后一次写入。 // 每次变化都先自增 installedRefreshGeneration:即使上一轮刷新仍在加载中,也会立即失效, // 避免用陈旧目录数据覆盖已安装列表(清理不单纯依赖定时器,代次校验兜底)。 +// initialCatalogLoaded:初始目录分批加载期间(apps.length 频繁变化)跳过此 watcher, +// 避免对未打开的模态框做无意义的已安装列表刷新 IPC。 +const initialCatalogLoaded = ref(false); let refreshDebounceTimer: ReturnType | null = null; watch( () => apps.value.length, (len) => { + if (!initialCatalogLoaded.value) return; if (!showInstalledModal.value || len <= 0) { return; } @@ -3234,12 +3238,15 @@ const loadApps = async (onFirstBatch?: () => void) => { ); // 增量式更新,让用户尽快看到部分数据 - apps.value.push(...normalizedApps); + // 用赋值替代 push(...),避免对响应式数组逐元素触发 re-render + apps.value = [...apps.value, ...normalizedApps]; // 只要有一个分类加载成功,就可以考虑关闭整体 loading(如果是首批逻辑) if (!firstBatchCallDone && typeof onFirstBatch === "function") { firstBatchCallDone = true; onFirstBatch(); + // 标记初始目录加载完成,使 apps.length watcher 开始在目录变更时刷新已安装列表 + initialCatalogLoaded.value = true; } } catch (error) { logger.warn(