refactor: improve preload security with IPC channel whitelist and fix dependencies

This commit is contained in:
xmk23333 2025-12-28 12:45:07 +08:00
parent 2467306903
commit a159974142
18 changed files with 284 additions and 43 deletions

View File

@ -40,7 +40,10 @@ module.exports = [
{
files: ['**/*.{ts,tsx}'],
rules: {
'@typescript-eslint/no-unused-vars': 0,
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' }
],
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'warn'
}

View File

@ -30,23 +30,16 @@
"build:linux:dev": "npm run prepare:dev && electron-vite build && electron-builder --publish never --linux"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.2",
"@electron-toolkit/utils": "^4.0.0",
"@heroui/react": "^2.8.5",
"@mihomo-party/sysproxy": "^2.0.8",
"@types/crypto-js": "^4.2.2",
"adm-zip": "^0.5.16",
"axios": "^1.13.2",
"chart.js": "^4.5.1",
"chokidar": "^4.0.3",
"croner": "^9.1.0",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.19",
"express": "^5.1.0",
"i18next": "^25.6.2",
"iconv-lite": "^0.6.3",
"react-chartjs-2": "^5.3.1",
"react-i18next": "^15.7.4",
"webdav": "^5.8.0",
"ws": "^8.18.3",
"yaml": "^2.8.1"
@ -58,8 +51,10 @@
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.1.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@heroui/react": "^2.8.5",
"@tailwindcss/vite": "^4.1.17",
"@types/adm-zip": "^0.5.7",
"@types/crypto-js": "^4.2.2",
"@types/express": "^5.0.5",
"@types/node": "^24.10.1",
"@types/pubsub-js": "^1.8.6",
@ -67,7 +62,9 @@
"@types/react-dom": "^19.2.3",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.7.0",
"chart.js": "^4.5.1",
"cron-validator": "^1.4.0",
"dayjs": "^1.11.19",
"driver.js": "^1.3.6",
"electron": "^37.10.0",
"electron-builder": "26.0.12",
@ -86,8 +83,10 @@
"prettier": "^3.6.2",
"pubsub-js": "^1.9.5",
"react": "^19.2.0",
"react-chartjs-2": "^5.3.1",
"react-dom": "^19.2.0",
"react-error-boundary": "^6.0.0",
"react-i18next": "^15.7.4",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
"react-monaco-editor": "^0.59.0",

View File

@ -562,7 +562,7 @@ async function waitForCoreReady(): Promise<void> {
await axios.get('/')
await managerLogger.info(`Core ready after ${i + 1} attempts (${(i + 1) * retryInterval}ms)`)
return
} catch (error) {
} catch {
if (i === 0) {
await managerLogger.info('Waiting for core to be ready...')
}
@ -791,7 +791,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
return true
}
} catch (error) {
} catch {
await managerLogger.info(
`Cannot get info for process ${pid}, might be high privilege`
)
@ -835,7 +835,7 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
}
}
}
} catch (error) {
} catch {
// ignore
}
}

View File

@ -15,7 +15,7 @@ export async function initProfileUpdater(): Promise<void> {
async () => {
try {
await addProfileItem(item)
} catch (e) {
} catch {
/* ignore */
}
},
@ -26,7 +26,7 @@ export async function initProfileUpdater(): Promise<void> {
intervalPool[item.id] = new Cron(item.interval, async () => {
try {
await addProfileItem(item)
} catch (e) {
} catch {
/* ignore */
}
})
@ -34,7 +34,7 @@ export async function initProfileUpdater(): Promise<void> {
try {
await addProfileItem(item)
} catch (e) {
} catch {
/* ignore */
}
}
@ -46,7 +46,7 @@ export async function initProfileUpdater(): Promise<void> {
async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
} catch {
/* ignore */
}
},
@ -57,7 +57,7 @@ export async function initProfileUpdater(): Promise<void> {
async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
} catch {
/* ignore */
}
},
@ -67,7 +67,7 @@ export async function initProfileUpdater(): Promise<void> {
intervalPool[currentItem.id] = new Cron(currentItem.interval, async () => {
try {
await addProfileItem(currentItem)
} catch (e) {
} catch {
/* ignore */
}
})
@ -75,7 +75,7 @@ export async function initProfileUpdater(): Promise<void> {
try {
await addProfileItem(currentItem)
} catch (e) {
} catch {
/* ignore */
}
}
@ -96,7 +96,7 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
async () => {
try {
await addProfileItem(item)
} catch (e) {
} catch {
/* ignore */
}
},
@ -106,7 +106,7 @@ export async function addProfileUpdater(item: IProfileItem): Promise<void> {
intervalPool[item.id] = new Cron(item.interval, async () => {
try {
await addProfileItem(item)
} catch (e) {
} catch {
/* ignore */
}
})

View File

@ -120,7 +120,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
groupsMenu = groupItems
groupsMenu.unshift({ type: 'separator' })
}
} catch (e) {
} catch {
// ignore
// 避免出错时无法创建托盘菜单
}
@ -206,7 +206,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
await patchAppConfig({ sysProxy: { enable } })
mainWindow?.webContents.send('appConfigUpdated')
floatingWindow?.webContents.send('appConfigUpdated')
} catch (e) {
} catch {
// ignore
} finally {
ipcMain.emit('updateTrayMenu')

View File

@ -61,7 +61,7 @@ export async function checkAutoRun(): Promise<boolean> {
`chcp 437 && %SystemRoot%\\System32\\schtasks.exe /query /tn "${appName}"`
)
return stdout.includes(appName)
} catch (e) {
} catch {
return false
}
}
@ -96,7 +96,7 @@ export async function enableAutoRun(): Promise<void> {
await execPromise(
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/create', '/tn', '${appName}', '/xml', '${taskFilePath}', '/f' -WindowStyle Hidden"`
)
} catch (e) {
} catch {
await managerLogger.info('Maybe the user rejected the UAC dialog?')
}
}
@ -144,7 +144,7 @@ export async function disableAutoRun(): Promise<void> {
await execPromise(
`powershell -NoProfile -Command "Start-Process schtasks -Verb RunAs -ArgumentList '/delete', '/tn', '${appName}', '/f' -WindowStyle Hidden"`
)
} catch (e) {
} catch {
await managerLogger.info('Maybe the user rejected the UAC dialog?')
}
}

View File

@ -1,6 +1,22 @@
import { ElectronAPI } from '@electron-toolkit/preload'
import { webUtils } from 'electron'
type IpcListener = (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
interface SafeIpcRenderer {
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>
send: (channel: string, ...args: unknown[]) => void
on: (channel: string, listener: IpcListener) => void
removeListener: (channel: string, listener: IpcListener) => void
removeAllListeners: (channel: string) => void
}
interface ElectronAPI {
ipcRenderer: SafeIpcRenderer
process: {
platform: NodeJS.Platform
}
}
declare global {
interface Window {
electron: ElectronAPI

View File

@ -1,13 +1,231 @@
import { contextBridge, webUtils } from 'electron'
import { electronAPI } from '@electron-toolkit/preload'
import { contextBridge, ipcRenderer, webUtils } from 'electron'
// 允许的 invoke channels 白名单
const validInvokeChannels = [
// Mihomo API
'mihomoVersion',
'mihomoCloseConnection',
'mihomoCloseAllConnections',
'mihomoRules',
'mihomoProxies',
'mihomoGroups',
'mihomoProxyProviders',
'mihomoUpdateProxyProviders',
'mihomoRuleProviders',
'mihomoUpdateRuleProviders',
'mihomoChangeProxy',
'mihomoUnfixedProxy',
'mihomoUpgradeGeo',
'mihomoUpgrade',
'mihomoUpgradeUI',
'mihomoUpgradeConfig',
'mihomoProxyDelay',
'mihomoGroupDelay',
'patchMihomoConfig',
'mihomoSmartGroupWeights',
'mihomoSmartFlushCache',
// AutoRun
'checkAutoRun',
'enableAutoRun',
'disableAutoRun',
// Config
'getAppConfig',
'patchAppConfig',
'getControledMihomoConfig',
'patchControledMihomoConfig',
'resetAppConfig',
// Profile
'getProfileConfig',
'setProfileConfig',
'getCurrentProfileItem',
'getProfileItem',
'getProfileStr',
'setProfileStr',
'addProfileItem',
'removeProfileItem',
'updateProfileItem',
'changeCurrentProfile',
'addProfileUpdater',
'removeProfileUpdater',
// Override
'getOverrideConfig',
'setOverrideConfig',
'getOverrideItem',
'addOverrideItem',
'removeOverrideItem',
'updateOverrideItem',
'getOverride',
'setOverride',
// File
'getFileStr',
'setFileStr',
'convertMrsRuleset',
'getRuntimeConfig',
'getRuntimeConfigStr',
'getSmartOverrideContent',
'getRuleStr',
'setRuleStr',
'getFilePath',
'readTextFile',
'openFile',
// Core
'restartCore',
'startMonitor',
'quitWithoutCore',
// System
'triggerSysProxy',
'checkTunPermissions',
'grantTunPermissions',
'manualGrantCorePermition',
'checkAdminPrivileges',
'restartAsAdmin',
'checkMihomoCorePermissions',
'requestTunPermissions',
'checkHighPrivilegeCore',
'showTunPermissionDialog',
'showErrorDialog',
'openUWPTool',
'setupFirewall',
'getInterfaces',
'setNativeTheme',
'copyEnv',
// Update
'checkUpdate',
'downloadAndInstallUpdate',
'getVersion',
'platform',
'fetchMihomoTags',
'installSpecificMihomoCore',
'clearMihomoVersionCache',
// Backup
'webdavBackup',
'webdavRestore',
'listWebdavBackups',
'webdavDelete',
'reinitWebdavBackupScheduler',
'exportLocalBackup',
'importLocalBackup',
// SubStore
'startSubStoreFrontendServer',
'stopSubStoreFrontendServer',
'startSubStoreBackendServer',
'stopSubStoreBackendServer',
'downloadSubStore',
'subStorePort',
'subStoreFrontendPort',
'subStoreSubs',
'subStoreCollections',
// Theme
'resolveThemes',
'fetchThemes',
'importThemes',
'readTheme',
'writeTheme',
'applyTheme',
// Tray
'showTrayIcon',
'closeTrayIcon',
'updateTrayIcon',
'updateTrayIconImmediate',
// Window
'showMainWindow',
'closeMainWindow',
'triggerMainWindow',
'showFloatingWindow',
'closeFloatingWindow',
'showContextMenu',
'setTitleBarOverlay',
'setAlwaysOnTop',
'isAlwaysOnTop',
'openDevTools',
'createHeapSnapshot',
'relaunchApp',
'quitApp',
// Shortcut
'registerShortcut',
// Misc
'getGistUrl',
'getImageDataURL',
'changeLanguage'
] as const
// 允许的 on/removeListener channels 白名单
const validListenChannels = [
'mihomoLogs',
'mihomoConnections',
'mihomoTraffic',
'mihomoMemory',
'appConfigUpdated',
'controledMihomoConfigUpdated',
'profileConfigUpdated',
'groupsUpdated',
'rulesUpdated'
] as const
// 允许的 send channels 白名单
const validSendChannels = [
'updateTrayMenu',
'updateFloatingWindow',
'trayIconUpdate'
] as const
type InvokeChannel = (typeof validInvokeChannels)[number]
type ListenChannel = (typeof validListenChannels)[number]
type SendChannel = (typeof validSendChannels)[number]
type IpcListener = (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
const listenerMap = new Map<ListenChannel, Set<IpcListener>>()
// 安全的 IPC API只暴露白名单内的 channels
const electronAPI = {
ipcRenderer: {
invoke: (channel: InvokeChannel, ...args: unknown[]): Promise<unknown> => {
if (validInvokeChannels.includes(channel)) {
return ipcRenderer.invoke(channel, ...args)
}
return Promise.reject(new Error(`Invalid invoke channel: ${channel}`))
},
send: (channel: SendChannel, ...args: unknown[]): void => {
if (validSendChannels.includes(channel)) {
ipcRenderer.send(channel, ...args)
}
},
on: (channel: ListenChannel, listener: IpcListener): void => {
if (validListenChannels.includes(channel)) {
if (!listenerMap.has(channel)) {
listenerMap.set(channel, new Set())
}
listenerMap.get(channel)!.add(listener)
ipcRenderer.on(channel, listener)
}
},
removeListener: (channel: ListenChannel, listener: IpcListener): void => {
if (validListenChannels.includes(channel)) {
listenerMap.get(channel)?.delete(listener)
ipcRenderer.removeListener(channel, listener)
}
},
removeAllListeners: (channel: ListenChannel): void => {
if (validListenChannels.includes(channel)) {
const listeners = listenerMap.get(channel)
if (listeners) {
listeners.forEach((listener) => {
ipcRenderer.removeListener(channel, listener)
})
listeners.clear()
}
}
}
},
process: {
platform: process.platform
}
}
// Custom APIs for renderer
const api = {
webUtils: webUtils
}
// Use `contextBridge` APIs to expose Electron APIs to
// renderer only if context isolation is enabled, otherwise
// just add to the DOM global.
if (process.contextIsolated) {
try {
contextBridge.exposeInMainWorld('electron', electronAPI)

View File

@ -86,7 +86,7 @@ const App: React.FC = () => {
options.color = window.getComputedStyle(document.documentElement).backgroundColor
options.symbolColor = window.getComputedStyle(document.documentElement).color
setTitleBarOverlay(options)
} catch (e) {
} catch {
// ignore
}
}

View File

@ -49,7 +49,8 @@ const FloatingApp: React.FC = () => {
}, [spinSpeed, spinFloatingIcon])
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => {
const info = args[0] as IMihomoTrafficInfo
setUpload(info.up)
setDownload(info.down)
})

View File

@ -32,7 +32,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
// @ts-ignore windowControlsOverlay
const windowControlsOverlay = window.navigator.windowControlsOverlay
setOverlayWidth(window.innerWidth - windowControlsOverlay.getTitlebarAreaRect().width)
} catch (e) {
} catch {
// ignore
}
}

View File

@ -149,7 +149,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
// 非纯数字
try {
setValues({ ...values, interval: v })
} catch (e) {
} catch {
// ignore
}
}

View File

@ -66,7 +66,7 @@ const Viewer: React.FC<Props> = (props) => {
})
)
}
} catch (error) {
} catch {
setCurrData(fileContent)
}
} finally {

View File

@ -126,7 +126,8 @@ const ConnCard: React.FC<Props> = (props) => {
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, info: IMihomoTrafficInfo) => {
window.electron.ipcRenderer.on('mihomoTraffic', async (_e, ...args) => {
const info = args[0] as IMihomoTrafficInfo
setUpload(info.up)
setDownload(info.down)
const data = series

View File

@ -43,7 +43,8 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
const token = PubSub.subscribe('mihomo-core-changed', () => {
mutate()
})
window.electron.ipcRenderer.on('mihomoMemory', (_e, info: IMihomoMemoryInfo) => {
window.electron.ipcRenderer.on('mihomoMemory', (_e, ...args) => {
const info = args[0] as IMihomoMemoryInfo
setMem(info.inuse)
})
return (): void => {

View File

@ -170,7 +170,8 @@ const Connections: React.FC = () => {
}
useEffect(() => {
const handler = (_e: unknown, info: IMihomoConnectionsInfo): void => {
const handler = (_e: unknown, ...args: unknown[]): void => {
const info = args[0] as IMihomoConnectionsInfo
setConnectionsInfo(info)
if (!info.connections) return

View File

@ -26,7 +26,8 @@ const cachedLogs: {
}
}
window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => {
window.electron.ipcRenderer.on('mihomoLogs', (_e, ...args) => {
const log = args[0] as IMihomoLogInfo
log.time = new Date().toLocaleString()
cachedLogs.log.push(log)
if (cachedLogs.log.length >= 500) {

View File

@ -1,8 +1,8 @@
import { TitleBarOverlayOptions } from 'electron'
function checkIpcError<T>(response: T | { invokeError: unknown }): T {
function checkIpcError<T>(response: unknown): T {
if (response && typeof response === 'object' && 'invokeError' in response) {
throw response.invokeError
throw (response as { invokeError: unknown }).invokeError
}
return response as T
}