mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
fix(安全/性能): AI审查实证修复 - metalinkUrl SSRF校验 + withTimeout定时器泄漏 + 批量更新/初始加载守卫
- 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改进项 ⑤⑥真实已修 ⑦⑧记录
This commit is contained in:
@@ -313,6 +313,20 @@ ipcMain.on("queue-install", async (event, download_json) => {
|
|||||||
return;
|
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}`);
|
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}, 来源: ${origin}`);
|
||||||
|
|
||||||
const webContents = event.sender;
|
const webContents = event.sender;
|
||||||
|
|||||||
+14
-7
@@ -509,17 +509,17 @@ const isInstalledAppInfo = (value: unknown): value is InstalledAppInfo => {
|
|||||||
const LIST_INSTALLED_TIMEOUT_MS = 15000;
|
const LIST_INSTALLED_TIMEOUT_MS = 15000;
|
||||||
|
|
||||||
// 为 Promise 增加超时控制:超时即 reject,配合 Promise.allSettled 让单来源失败不影响其它来源。
|
// 为 Promise 增加超时控制:超时即 reject,配合 Promise.allSettled 让单来源失败不影响其它来源。
|
||||||
|
// 注意:promise 先 resolve 时必须 clearTimeout,否则 setTimeout 句柄泄漏。
|
||||||
const withTimeout = <T,>(
|
const withTimeout = <T,>(
|
||||||
promise: Promise<T>,
|
promise: Promise<T>,
|
||||||
ms: number,
|
ms: number,
|
||||||
label: string,
|
label: string,
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
return Promise.race([
|
let timer: ReturnType<typeof setTimeout>;
|
||||||
promise,
|
const timeout = new Promise<T>((_, reject) => {
|
||||||
new Promise<T>((_, reject) =>
|
timer = setTimeout(() => reject(new Error(`${label} 超时(${ms}ms)`)), ms);
|
||||||
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 防抖合并最后一次写入。
|
// 现改为 len 任意 >0 的正向变化都允许触发;密集分批推送由下方 300ms 防抖合并最后一次写入。
|
||||||
// 每次变化都先自增 installedRefreshGeneration:即使上一轮刷新仍在加载中,也会立即失效,
|
// 每次变化都先自增 installedRefreshGeneration:即使上一轮刷新仍在加载中,也会立即失效,
|
||||||
// 避免用陈旧目录数据覆盖已安装列表(清理不单纯依赖定时器,代次校验兜底)。
|
// 避免用陈旧目录数据覆盖已安装列表(清理不单纯依赖定时器,代次校验兜底)。
|
||||||
|
// initialCatalogLoaded:初始目录分批加载期间(apps.length 频繁变化)跳过此 watcher,
|
||||||
|
// 避免对未打开的模态框做无意义的已安装列表刷新 IPC。
|
||||||
|
const initialCatalogLoaded = ref(false);
|
||||||
let refreshDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
let refreshDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
watch(
|
watch(
|
||||||
() => apps.value.length,
|
() => apps.value.length,
|
||||||
(len) => {
|
(len) => {
|
||||||
|
if (!initialCatalogLoaded.value) return;
|
||||||
if (!showInstalledModal.value || len <= 0) {
|
if (!showInstalledModal.value || len <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3234,12 +3238,15 @@ const loadApps = async (onFirstBatch?: () => void) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 增量式更新,让用户尽快看到部分数据
|
// 增量式更新,让用户尽快看到部分数据
|
||||||
apps.value.push(...normalizedApps);
|
// 用赋值替代 push(...),避免对响应式数组逐元素触发 re-render
|
||||||
|
apps.value = [...apps.value, ...normalizedApps];
|
||||||
|
|
||||||
// 只要有一个分类加载成功,就可以考虑关闭整体 loading(如果是首批逻辑)
|
// 只要有一个分类加载成功,就可以考虑关闭整体 loading(如果是首批逻辑)
|
||||||
if (!firstBatchCallDone && typeof onFirstBatch === "function") {
|
if (!firstBatchCallDone && typeof onFirstBatch === "function") {
|
||||||
firstBatchCallDone = true;
|
firstBatchCallDone = true;
|
||||||
onFirstBatch();
|
onFirstBatch();
|
||||||
|
// 标记初始目录加载完成,使 apps.length watcher 开始在目录变更时刷新已安装列表
|
||||||
|
initialCatalogLoaded.value = true;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
Reference in New Issue
Block a user