主题切换
附录 E:Electron 安全最佳实践清单
安全不是可选特性,而是桌面应用的基础要求。本附录提供一份可直接执行的 Electron 安全检查清单,覆盖从开发到发布的全流程。
E.1 Electron 安全威胁模型
在 Electron 应用中,主进程是信任边界——它拥有完整的系统访问权限。安全策略的核心是:渲染进程中的代码不可信,必须通过最小权限原则限制其能力。
text
┌──────────────────┐
│ 外部攻击者 │
│ (XSS, 远程代码) │
└────────┬─────────┘
│
┌──────────────▼──────────────┐
│ 渲染进程 │
│ ┌──────────────────────┐ │
│ │ 不可信页面代码 │ │
│ │ (HTML/CSS/JS) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌──────────▼───────────┐ │
│ │ contextBridge 网关 │ │
│ │ (白名单 API 暴露) │ │
│ └──────────┬───────────┘ │
└─────────────┼───────────────┘
│ IPC
┌─────────────▼───────────────┐
│ 主进程 │
│ ┌──────────────────────┐ │
│ │ 完整系统权限 │ │
│ │ fs, net, shell, ... │ │
│ │ ← 所有 IPC 调用都需 │ │
│ │ 验证参数合法性 │ │
│ └──────────────────────┘ │
└─────────────────────────────┘关键威胁
| 威胁类型 | 攻击路径 | 严重程度 |
|---|---|---|
| XSS 注入 | 用户输入未过滤,恶意脚本在渲染进程中执行 | 高 |
| IPC 滥用 | 暴露过度宽泛的 IPC 接口,允许渲染进程执行任意系统命令 | 严重 |
| 依赖劫持 | npm 包被恶意篡改,供应链投毒 | 高 |
| 远程内容加载 | 加载不受信的远程 URL,攻击者可控制页面内容 | 严重 |
| ASAR 篡改 | 应用打包后被人为修改,注入恶意代码 | 中 |
| 敏感数据泄露 | 本地存储的密钥/Token 未加密 | 高 |
E.2 安全配置检查清单
BrowserWindow 配置
javascript
// ✅ 推荐:生产环境安全配置
const mainWindow = new BrowserWindow({
webPreferences: {
sandbox: true, // 必须:操作系统级沙箱
contextIsolation: true, // 必须:上下文隔离
nodeIntegration: false, // 必须:关闭 Node 集成
webSecurity: true, // 必须:同源策略
allowRunningInsecureContent: false, // 必须:禁止混合内容
autoplayPolicy: 'user-gesture', // 推荐:限制自动播放
preload: path.join(__dirname, 'preload.js')
}
})| 配置项 | 安全值 | 检查 |
|---|---|---|
sandbox | true | ☐ |
contextIsolation | true | ☐ |
nodeIntegration | false | ☐ |
webSecurity | true | ☐ |
allowRunningInsecureContent | false | ☐ |
| 是否加载远程 URL | 禁止(仅加载本地文件) | ☐ |
preload 脚本 API | 最小暴露(逐个方法白名单) | ☐ |
Preload 脚本检查
typescript
import { contextBridge, ipcRenderer } from 'electron'
// ✅ 正确:逐个方法暴露,附带参数验证
contextBridge.exposeInMainWorld('electronAPI', {
openFile: () => ipcRenderer.invoke('dialog:openFile'),
saveFile: (content: string) => {
// 渲染进程校验
if (typeof content !== 'string' || content.length > 10 * 1024 * 1024) {
throw new Error('内容过大或类型错误')
}
return ipcRenderer.invoke('file:save', content)
},
getAppVersion: () => ipcRenderer.invoke('app:version'),
})
// ❌ 错误:直接暴露整个 ipcRenderer
contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer: { ...ipcRenderer } // 危险!允许任意 IPC 调用
})IPC Handler 安全检查
typescript
import { ipcMain, dialog } from 'electron'
ipcMain.handle('file:save', async (_event, content: unknown) => {
// 1. 类型校验
if (typeof content !== 'string') {
throw new Error('Invalid content type')
}
// 2. 大小限制
if (content.length > 10 * 1024 * 1024) {
throw new Error('Content too large')
}
// 3. 路径白名单
const result = await dialog.showSaveDialog({
defaultPath: 'document.txt',
filters: [{ name: '文本文件', extensions: ['txt'] }]
})
if (result.canceled) return false
// 4. 确认路径在允许的目录内
if (!result.filePath!.startsWith(app.getPath('documents'))) {
throw new Error('Unauthorized save path')
}
await fs.promises.writeFile(result.filePath!, content, 'utf-8')
return true
})E.3 CSP 推荐配置
严格策略(推荐)
html
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: file:;
font-src 'self' data:;
connect-src 'self' https://your-api.com;
media-src 'none';
object-src 'none';
base-uri 'self';
form-action 'none';
">通过主进程动态注入 CSP
javascript
app.whenReady().then(() => {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; script-src 'self'; connect-src 'self' https://api.example.com"
]
}
})
})
})E.4 safeStorage 加密敏感数据
Electron 42 的 safeStorage API 使用操作系统级加密存储敏感信息(API Key、Token 等):
typescript
import { safeStorage } from 'electron'
import Store from 'electron-store'
const store = new Store({ name: 'secrets', encryptionKey: 'my-key' })
// 加密存储
function saveSecret(key: string, value: string): void {
if (safeStorage.isEncryptionAvailable()) {
const encrypted = safeStorage.encryptString(value)
store.set(key, encrypted.toString('base64'))
} else {
throw new Error('系统不支持安全加密')
}
}
// 解密读取
function getSecret(key: string): string {
const encryptedBase64 = store.get(key) as string
if (!encryptedBase64) throw new Error(`Secret not found: ${key}`)
const encrypted = Buffer.from(encryptedBase64, 'base64')
return safeStorage.decryptString(encrypted)
}
// 使用示例
saveSecret('openai-api-key', 'sk-xxxxx')
const apiKey = getSecret('openai-api-key')加密最佳实践
safeStorage在 macOS 使用 Keychain,Windows 使用 DPAPI,Linux 使用 libsecret- 加密前检查
safeStorage.isEncryptionAvailable()—— 某些 Linux 发行版可能不可用 - 不要在日志中输出解密后的密钥值
- 定期轮换敏感凭据
E.5 依赖供应链安全
bash
# 检查已知漏洞
npm audit
# 自动修复(不会引入 breaking changes)
npm audit fix
# 锁定依赖版本(CI 中验证)
npm ci
# 检查依赖许可证
npx license-checker --summarypackage.json 安全配置
json
{
"scripts": {
"preinstall": "npx only-allow pnpm",
"postinstall": "electron-rebuild"
},
"overrides": {
"minimist": ">=1.2.6"
},
"engines": {
"node": ">=22.0.0",
"pnpm": ">=9.0.0"
}
}E.6 代码签名与 ASAR 完整性
yaml
win:
signingHashAlgorithms: ['sha256']
certificateFile: cert.pfx
certificatePassword: ${CERT_PASSWORD}
mac:
hardenedRuntime: true
gatekeeperAssess: false
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
# ASAR 完整性检查
asar:
smartUnpack: trueE.7 网络安全
javascript
const { session } = require('electron')
// 限制请求域白名单
session.defaultSession.webRequest.onBeforeRequest(
{ urls: ['*://*/*'] },
(details, callback) => {
const url = new URL(details.url)
const allowedHosts = ['api.yourapp.com', 'cdn.yourapp.com']
if (!allowedHosts.includes(url.hostname)) {
callback({ cancel: true })
return
}
callback({})
}
)
// 证书验证 — 始终启用
app.on('certificate-error', (event, _webContents, _url, _error, _cert, callback) => {
event.preventDefault()
callback(false) // 拒绝不受信的证书
})E.8 安全检查清单总览
| 类别 | 检查项 | 状态 |
|---|---|---|
| 窗口安全 | sandbox: true | ☐ |
| contextIsolation: true | ☐ | |
| nodeIntegration: false | ☐ | |
| Preload | 仅暴露最小必要 API | ☐ |
| 每个 IPC 方法有参数校验 | ☐ | |
| IPC | Handler 中验证参数类型和范围 | ☐ |
| 文件路径白名单校验 | ☐ | |
| 禁止 shell.openExternal 打开不受信 URL | ☐ | |
| CSP | 通过 meta 或 header 配置 | ☐ |
| 禁止 unsafe-eval | ☐ | |
| connect-src 限定已知 API 域名 | ☐ | |
| 数据 | 敏感数据使用 safeStorage 加密 | ☐ |
| 日志不含敏感信息 | ☐ | |
| 依赖 | npm audit 无严重漏洞 | ☐ |
| 锁定文件 (pnpm-lock.yaml) 已提交 | ☐ | |
| 签名 | Windows 签名证书有效 | ☐ |
| macOS 启用 Hardened Runtime | ☐ | |
| 网络 | 请求白名单过滤 | ☐ |
| 拒绝不受信 SSL 证书 | ☐ |