From cdd94c650ddfaf518fc73548839ec63b038c5066 Mon Sep 17 00:00:00 2001 From: xiyidaiwa Date: Thu, 13 Aug 2026 22:38:26 +0800 Subject: [PATCH] =?UTF-8?q?fix(security):=20PR=20=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=E6=95=B4=E6=94=B9=20-=20setWindowOpenHandler=20=E5=9F=9F?= =?UTF-8?q?=E5=90=8D=E7=99=BD=E5=90=8D=E5=8D=95=20/=20openWebsite=20?= =?UTF-8?q?=E5=8D=8F=E8=AE=AE=E6=A0=A1=E9=AA=8C=20/=20=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E4=BA=91=E7=AB=AF=E5=AE=89=E8=A3=85=E5=B9=B6=E5=8F=91=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - electron/main/index.ts: setWindowOpenHandler 增加 ALLOWED_EXTERNAL_HOSTS 域名后缀白名单,仅可信 https 域名可 shell.openExternal(阻断项2) - AppDetailModal.vue: openWebsite 增加 http/https 协议校验 + noopener,noreferrer(改进项5) - useAccountSync.ts: installCloudItems 改为分批(BATCH_SIZE=3) + Promise.allSettled 错误收集(改进项2) - 审查误报/未改动项已贴代码实证:阻断1 v-html 已用 textContent 转义;改进3 resolveCloudInstallCandidate 已有降级匹配链;改进4 单窗口无 HMR 不重复注册;改进1 暂停/恢复主进程无 handler 维持 TODO --- debian/changelog | 2 +- electron/main/index.ts | 21 +++++++++++++++++++-- src/components/AppDetailModal.vue | 11 +++++++++-- src/composables/useAccountSync.ts | 23 ++++++++++++++++++----- 4 files changed, 47 insertions(+), 10 deletions(-) diff --git a/debian/changelog b/debian/changelog index 9c31ee1b..55118f5c 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -spark-store (5.2.1.5-test) UNRELEASED; urgency=medium +spark-store (5.2.1.6-test) UNRELEASED; urgency=medium * Initial release. (Closes: #nnnn) diff --git a/electron/main/index.ts b/electron/main/index.ts index c9cb8616..6b514806 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -473,9 +473,26 @@ async function createWindow() { logger.info("Renderer process is ready."); }); - // Make all links open with the browser, not with the application + // 仅允许可信域名的 https 链接通过浏览器打开,避免钓鱼/恶意站。 + // 协议前缀 + 域名后缀白名单双重校验;非法/无效 URL 一律拒绝。 + const ALLOWED_EXTERNAL_HOSTS = [ + "spark-app.store", + "gitee.com", + "bbs.spark-app.store", + "spark-app.cn", + ]; mainWindow.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith("https:")) shell.openExternal(url); + try { + const parsed = new URL(url); + if ( + parsed.protocol === "https:" && + ALLOWED_EXTERNAL_HOSTS.some((h) => parsed.hostname === h || parsed.hostname.endsWith(`.${h}`)) + ) { + shell.openExternal(url); + } + } catch { + // 无效 URL,拒绝打开 + } return { action: "deny" }; }); // win.webContents.on('will-navigate', (event, url) => { }) #344 diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue index d6743456..93c791b1 100644 --- a/src/components/AppDetailModal.vue +++ b/src/components/AppDetailModal.vue @@ -644,8 +644,15 @@ const closeMetaModal = () => { }; const openWebsite = (url: string) => { - if (url) { - window.open(url, "_blank"); + if (!url) return; + try { + const parsed = new URL(url); + // 仅允许 http/https 协议,杜绝 javascript:/data: 等危险协议 + if (parsed.protocol === "https:" || parsed.protocol === "http:") { + window.open(url, "_blank", "noopener,noreferrer"); + } + } catch { + // 无效 URL,不打开 } }; diff --git a/src/composables/useAccountSync.ts b/src/composables/useAccountSync.ts index 99bf97c8..1dc3c466 100644 --- a/src/composables/useAccountSync.ts +++ b/src/composables/useAccountSync.ts @@ -319,11 +319,24 @@ const openRestoreFromAccount = async (): Promise => { } }; -const installCloudItems = (items: SyncedAppListItem[]): void => { - for (const item of items) { - const app = resolveCloudInstallCandidate(item, apps.value); - if (!app) continue; - void onDetailInstall(app); +// 批量云端安装:分批触发(每批 3 个),收集每个安装的结果并反馈失败项。 +// onDetailInstall 内部本身已串行排队,这里仅限制"同时发起"的并发,避免一次注入大量任务。 +const installCloudItems = async (items: SyncedAppListItem[]): Promise => { + const BATCH_SIZE = 3; + let failedCount = 0; + for (let i = 0; i < items.length; i += BATCH_SIZE) { + const batch = items.slice(i, i + BATCH_SIZE); + const results = await Promise.allSettled( + batch.map(async (item) => { + const app = resolveCloudInstallCandidate(item, apps.value); + if (!app) return; + await onDetailInstall(app); + }), + ); + failedCount += results.filter((r) => r.status === "rejected").length; + } + if (failedCount > 0) { + console.error(`批量云端安装中有 ${failedCount} 项失败`); } showRestoreModal.value = false; };