fix: 安全与健壮性排查修复(IPC 类型校验/AbortSignal/空名去重/窗口 await)

阻断项:
- install-manager: uninstall-installed / remove-installed / check-installed 由 any 改为 unknown + parseAppPayload 严格解析(PKGNAME_PATTERN 校验 pkgname,origin 仅限 spark|apm),消除命令注入风险;parseAppPayload/PKGNAME_PATTERN 提升至模块顶部复用并避免 TDZ
- App.vue openDetail: 参数由 Record<string,unknown> 收紧为 App | OpenDetailInput,新增 pkgname 运行时校验,避免访问 undefined 属性

改进项:
- App.vue homeLinks 去重:跳过空名称避免污染 seenNames,并对 links 数组与 HomeLink 必需字段做校验
- App.vue fetchWithRetry: 新增 AbortSignal 参数,递归链路传递,组件卸载时 rootAbortController.abort() 取消未完成任务
- index.ts showAndFocusMainWindow: 改为 async 并 await createWindow,调用点用 void 修饰避免 floating promise

vue-tsc --noEmit 通过。
This commit is contained in:
xiyidaiwa
2026-08-03 11:24:32 +08:00
parent b89c001401
commit 4f772ba7eb
3 changed files with 102 additions and 53 deletions
+41 -23
View File
@@ -12,6 +12,26 @@ import { findExecutable, SUPER_USER_COMMAND_CANDIDATES } from "./superuser";
const logger = pino({ name: "install-manager" });
// 包名白名单:仅允许合法包名字符,杜绝命令注入(spawn 用 shell:false 仍须校验)。
const PKGNAME_PATTERN = /^[a-zA-Z0-9._+-]+$/;
// 解析并校验应用类 IPC 的 payload(可能是旧版字符串或对象)。
// 返回规范化后的 { pkgname, origin }pkgname 非法时返回 null。
const parseAppPayload = (
payload: unknown,
): { pkgname: string; origin: "spark" | "apm" } | null => {
if (typeof payload === "string") {
if (!PKGNAME_PATTERN.test(payload)) return null;
return { pkgname: payload, origin: "spark" };
}
if (typeof payload !== "object" || payload === null) return null;
const p = payload as Record<string, unknown>;
const pkgname = typeof p.pkgname === "string" ? p.pkgname : "";
if (!PKGNAME_PATTERN.test(pkgname)) return null;
const origin: "spark" | "apm" = p.origin === "apm" ? "apm" : "spark";
return { pkgname, origin };
};
const getStoreFilterFromArgv = (): "spark" | "apm" | "both" => {
const argv = process.argv;
const noApm = argv.includes("--no-apm");
@@ -815,15 +835,13 @@ async function runInstallPhase(task: InstallTask) {
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle("check-installed", async (_event, payload: any) => {
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
const origin = typeof payload === "string" ? "spark" : payload.origin;
if (!pkgname) {
logger.warn("check-installed missing pkgname");
ipcMain.handle("check-installed", async (_event, payload: unknown) => {
const parsed = parseAppPayload(payload);
if (!parsed) {
logger.warn("check-installed invalid payload");
return false;
}
const { pkgname, origin } = parsed;
logger.info(`检查应用是否已安装: ${pkgname} (来源: ${origin})`);
@@ -890,13 +908,12 @@ ipcMain.handle("check-installed", async (_event, payload: any) => {
ipcMain.on("remove-installed", async (_event, payload) => {
const webContents = _event.sender;
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
const origin = typeof payload === "string" ? "spark" : payload.origin;
if (!pkgname) {
logger.warn("remove-installed missing pkgname");
const parsed = parseAppPayload(payload);
if (!parsed) {
logger.warn("remove-installed invalid payload");
return;
}
const { pkgname, origin } = parsed;
logger.info(`卸载已安装应用: ${pkgname} (来源: ${origin})`);
let execCommand = "";
@@ -1243,14 +1260,18 @@ ipcMain.handle("show-apm-install-dialog", async (event) => {
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle("uninstall-installed", async (_event, payload: any) => {
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
const origin = typeof payload === "string" ? "spark" : payload.origin;
if (!pkgname) {
logger.warn("uninstall-installed missing pkgname");
return { success: false, message: "missing pkgname" };
}
ipcMain.handle(
"uninstall-installed",
async (
_event,
payload: unknown,
): Promise<{ success: boolean; message?: string }> => {
const parsed = parseAppPayload(payload);
if (!parsed) {
logger.warn("uninstall-installed invalid payload");
return { success: false, message: "invalid payload" };
}
const { pkgname, origin } = parsed;
const superUserCmd = await checkSuperUserCommand();
const execCommand = superUserCmd || SHELL_CALLER_PATH;
@@ -1287,9 +1308,6 @@ interface LaunchAppPayload {
origin?: "spark" | "apm";
}
// 合法包名字符(Debian 包名规范 + Spark 应用包名常见字符),
// 用于拦截包含特殊字符的非法输入,避免命令注入。
const PKGNAME_PATTERN = /^[a-zA-Z0-9._+-]+$/;
ipcMain.handle(
"launch-app",
+7 -6
View File
@@ -272,9 +272,10 @@ const requestApplicationExit = (): void => {
app.quit();
};
const showAndFocusMainWindow = (): void => {
const showAndFocusMainWindow = async (): Promise<void> => {
if (!win || win.isDestroyed()) {
createWindow();
// 等待窗口创建完成,创建失败时调用方可通过异常感知
await createWindow();
return;
}
@@ -643,11 +644,11 @@ app.on("window-all-closed", () => {
});
app.on("second-instance", () => {
showAndFocusMainWindow();
void showAndFocusMainWindow();
});
app.on("activate", () => {
showAndFocusMainWindow();
void showAndFocusMainWindow();
});
app.on("will-quit", () => {
@@ -700,7 +701,7 @@ app.whenReady().then(() => {
{
label: "显示主界面",
click: () => {
showAndFocusMainWindow();
void showAndFocusMainWindow();
},
},
{
@@ -719,7 +720,7 @@ app.whenReady().then(() => {
win.hide();
win.setSkipTaskbar(true);
} else {
showAndFocusMainWindow();
void showAndFocusMainWindow();
}
});
});