跳到主要内容

安全最佳实践

Electron 应用拥有比 Web 应用更大的权限,因此安全问题尤为重要。本节介绍 Electron 应用的安全最佳实践。

安全风险

Electron 应用可以访问文件系统、执行系统命令、访问原生 API。如果加载了恶意内容,攻击者可能:

  1. 读取敏感文件
  2. 执行任意代码
  3. 访问用户数据
  4. 发起网络请求

核心安全原则

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: ''
}
})

配置说明

配置项推荐值说明
nodeIntegrationfalse禁用 Node.js 集成
contextIsolationtrue启用上下文隔离
sandboxtrue启用沙箱
webSecuritytrue启用同源策略
allowRunningInsecureContentfalse禁止加载不安全内容
experimentalFeaturesfalse禁用实验性功能

内容安全策略

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-srcJavaScript 来源
style-srcCSS 来源
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 攻击

跨站脚本攻击可以窃取用户数据或执行恶意操作。

防护措施

  1. 不要使用 innerHTML
  2. 对用户输入进行转义
  3. 设置严格的 CSP

远程代码执行

如果启用了 nodeIntegration,XSS 可能导致远程代码执行。

防护措施

  1. 禁用 nodeIntegration
  2. 启用 contextIsolation
  3. 不要加载远程内容

中间人攻击

HTTP 连接可能被劫持。

防护措施

  1. 只使用 HTTPS
  2. 设置 webSecurity: true
  3. 不要禁用证书验证

总结

Electron 应用安全需要从多个层面考虑:

  1. 配置安全:正确设置 webPreferences
  2. 内容安全:只加载可信内容,设置 CSP
  3. 通信安全:验证 IPC 消息,使用白名单
  4. 导航安全:限制导航和新窗口
  5. 依赖安全:保持更新,检查漏洞

安全是一个持续的过程,需要在开发的每个阶段都保持警惕。