安全最佳实践
Electron 应用拥有比 Web 应用更大的权限,因此安全问题尤为重要。本节介绍 Electron 应用的安全最佳实践。
安全风险
Electron 应用可以访问文件系统、执行系统命令、访问原生 API。如果加载了恶意内容,攻击者可能:
- 读取敏感文件
- 执行任意代码
- 访问用户数据
- 发起网络请求
核心安全原则
1. 只加载可信内容
永远不要加载和执行来自不可信来源的远程代码。
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false
}
})
mainWindow.loadFile('index.html')
如果必须加载远程内容,确保使用 HTTPS:
mainWindow.loadURL('https://example.com')
2. 禁用 Node.js 集成
默认情况下,渲染进程不应该有 Node.js 访问权限。
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true
}
})
3. 启用上下文隔离
上下文隔离确保预加载脚本和渲染进程运行在不同的 JavaScript 上下文中。
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
sandbox: true
}
})
4. 启用进程沙箱
沙箱限制了渲染进程的系统能力。
const mainWindow = new BrowserWindow({
webPreferences: {
sandbox: true
}
})
5. 使用安全的预加载脚本
不要直接暴露整个 ipcRenderer:
const { contextBridge, ipcRenderer } = require('electron')
// 错误做法
contextBridge.exposeInMainWorld('electron', {
ipcRenderer: ipcRenderer
})
// 正确做法
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
const validChannels = ['toMain']
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data)
}
}
})
安全配置清单
webPreferences 配置
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
experimentalFeatures: false,
enableBlinkFeatures: '',
disableBlinkFeatures: ''
}
})
配置说明
| 配置项 | 推荐值 | 说明 |
|---|---|---|
nodeIntegration | false | 禁用 Node.js 集成 |
contextIsolation | true | 启用上下文隔离 |
sandbox | true | 启用沙箱 |
webSecurity | true | 启用同源策略 |
allowRunningInsecureContent | false | 禁止加载不安全内容 |
experimentalFeatures | false | 禁用实验性功能 |
内容安全策略
CSP(Content Security Policy)可以限制资源加载来源。
设置 CSP
const { session } = require('electron')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'",
"script-src 'self'",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data:",
"connect-src 'self' https://api.example.com"
].join('; ')
}
})
})
HTML meta 标签
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
CSP 指令说明
| 指令 | 说明 |
|---|---|
default-src | 默认资源来源 |
script-src | JavaScript 来源 |
style-src | CSS 来源 |
img-src | 图片来源 |
connect-src | 网络请求来源 |
font-src | 字体来源 |
IPC 安全
验证发送者
ipcMain.handle('get-secrets', (event) => {
const senderFrame = event.senderFrame
const url = new URL(senderFrame.url)
if (url.origin !== 'file://') {
return null
}
return getSecrets()
})
白名单通道
const validChannels = new Set([
'file:open',
'file:save',
'user:login'
])
contextBridge.exposeInMainWorld('electronAPI', {
send: (channel, data) => {
if (validChannels.has(channel)) {
ipcRenderer.send(channel, data)
}
},
invoke: (channel, data) => {
if (validChannels.has(channel)) {
return ipcRenderer.invoke(channel, data)
}
return Promise.reject(new Error('Invalid channel'))
}
})
不要信任渲染进程数据
ipcMain.handle('file:read', async (event, filePath) => {
const resolvedPath = path.resolve(filePath)
const appPath = app.getAppPath()
if (!resolvedPath.startsWith(appPath)) {
throw new Error('Access denied')
}
return fs.promises.readFile(resolvedPath, 'utf-8')
})
导航安全
限制导航
const { URL } = require('node:url')
mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl)
if (parsedUrl.origin !== 'https://example.com') {
event.preventDefault()
}
})
限制新窗口
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
const parsedUrl = new URL(url)
if (parsedUrl.protocol === 'https:') {
shell.openExternal(url)
}
return { action: 'deny' }
})
WebView 安全
如果使用 <webview> 标签,需要特别注意安全:
验证 WebView 配置
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (event, webPreferences, params) => {
delete webPreferences.preload
webPreferences.nodeIntegration = false
webPreferences.contextIsolation = true
if (!params.src.startsWith('https://example.com/')) {
event.preventDefault()
}
})
})
禁用不安全的属性
<!-- 不要使用这些属性 -->
<webview nodeIntegration src="..."></webview>
<webview allowpopups src="..."></webview>
<!-- 安全的做法 -->
<webview src="https://example.com"></webview>
shell 模块安全
不要使用 shell.openExternal 打开不可信的 URL:
const { shell } = require('electron')
const { URL } = require('node:url')
function openExternal(url) {
const parsedUrl = new URL(url)
const allowedProtocols = ['http:', 'https:']
if (allowedProtocols.includes(parsedUrl.protocol)) {
shell.openExternal(url)
}
}
安全更新
保持 Electron 更新
npm update electron
检查依赖漏洞
npm audit
npm audit fix
使用最新 LTS 版本
Electron 定期发布安全更新,确保使用最新的稳定版本。
安全检查清单
在发布应用前,检查以下项目:
-
nodeIntegration设置为false -
contextIsolation设置为true -
sandbox设置为true -
webSecurity设置为true - 只加载 HTTPS 内容
- 设置了 CSP
- IPC 通道使用白名单
- 验证了 IPC 消息发送者
- 限制了导航和新窗口
- 使用最新版本的 Electron
- 检查了依赖漏洞
常见安全问题
XSS 攻击
跨站脚本攻击可以窃取用户数据或执行恶意操作。
防护措施:
- 不要使用
innerHTML - 对用户输入进行转义
- 设置严格的 CSP
远程代码执行
如果启用了 nodeIntegration,XSS 可能导致远程代码执行。
防护措施:
- 禁用
nodeIntegration - 启用
contextIsolation - 不要加载远程内容
中间人攻击
HTTP 连接可能被劫持。
防护措施:
- 只使用 HTTPS
- 设置
webSecurity: true - 不要禁用证书验证
总结
Electron 应用安全需要从多个层面考虑:
- 配置安全:正确设置 webPreferences
- 内容安全:只加载可信内容,设置 CSP
- 通信安全:验证 IPC 消息,使用白名单
- 导航安全:限制导航和新窗口
- 依赖安全:保持更新,检查漏洞
安全是一个持续的过程,需要在开发的每个阶段都保持警惕。