Electron 的安全模型基于 Chromium 的多进程架构和 Node.js 的集成。理解安全模型对于构建安全的 Electron 应用至关重要。本文档详细介绍 Electron 的安全架构、核心安全配置及最佳实践。
Electron 采用多进程架构,继承自 Chromium 的设计理念。每个进程有明确的职责边界和安全限制。
┌─────────────────────────────────────────────────────────────┐
│ Main Process │
│ 主进程 - 单一实例 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 完全访问 Node.js 和 Electron API │ │
│ │ • 管理窗口(BrowserWindow)和应用生命周期 │ │
│ │ • 处理系统级操作(文件、原生菜单、对话框等) │ │
│ │ • 作为唯一可信源处理敏感操作 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ IPC (进程间通信 - 异步消息传递)
▼
┌─────────────────────────────────────────────────────────────┐
│ Preload Script │
│ 预加载脚本 - 每个渲染进程一个 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 在渲染进程创建前执行 │ │
│ │ • 访问有限的 Node.js API 子集 │ │
│ │ • 通过 contextBridge 安全暴露 API 给渲染进程 │ │
│ │ • 作为主进程与渲染进程之间的桥梁 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ contextBridge.exposeInMainWorld()
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer Process │
│ 渲染进程 - 可多个实例 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ • 运行 Web 内容(HTML/CSS/JavaScript) │ │
│ │ • 默认无 Node.js 访问权限 │ │
│ │ • 通过暴露的 API 与主进程通信 │ │
│ │ • 可能加载不可信内容,需要严格隔离 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘┌──────────────┐ IPC 消息验证 ┌──────────────┐
│ Renderer │ ──────────────────► │ Main │
│ (不可信区) │ 严格校验参数 │ (可信区) │
│ │ ◄────────────────── │ │
└──────────────┘ 返回安全数据 └──────────────┘
│ │
│ contextBridge │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Preload.js │ │ Node.js API │
│ (受限访问) │ │ (完全访问) │
└──────────────┘ └──────────────┘| 职责 | 说明 | 安全注意 |
|---|---|---|
| 应用生命周期 | 管理 app 模块的生命周期事件 | 避免在 will-quit 前泄露敏感数据 |
| 窗口管理 | 创建和管理 BrowserWindow 实例 | 严格配置 webPreferences |
| IPC 处理 | 处理来自渲染进程的请求 | 验证所有输入参数 |
| 原生集成 | 菜单、托盘、快捷键等 | 权限最小化原则 |
| 文件系统 | 读写文件、访问数据库 | 路径验证、权限控制 |
| 职责 | 说明 | 安全注意 |
|---|---|---|
| API 暴露 | 通过 contextBridge 暴露安全 API | 使用白名单机制 |
| 数据转换 | 处理渲染进程与主进程间的数据传递 | 过滤敏感信息 |
| 权限控制 | 限制渲染进程可执行的操作 | 最小暴露原则 |
| 职责 | 说明 | 安全注意 |
|---|---|---|
| UI 渲染 | 渲染用户界面 | 防止 XSS 攻击 |
| 用户交互 | 处理用户输入 | 输入验证和清理 |
| 网络请求 | 发送 HTTP 请求(如有权限) | 使用 HTTPS |
| 参数 | 推荐值 | 默认值 | 说明 |
|---|---|---|---|
contextIsolation | true | true (v12+) | 上下文隔离 |
nodeIntegration | false | false | Node.js 集成 |
sandbox | true | true (v20+) | 沙箱模式 |
webSecurity | true | true | Web 安全策略 |
allowRunningInsecureContent | false | false | 允许混合内容 |
enableRemoteModule | false | 已于 Electron 14 移除 | 远程模块(已废弃,无需配置) |
nodeIntegrationInWorker | false | false | Worker 中 Node 集成 |
nodeIntegrationInSubFrames | false | false | 子框架中 Node 集成 |
状态:从 Electron v12 开始默认启用
必须启用,这是最重要的安全设置:
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true // 默认为 true,强烈建议保持
}
})┌─────────────────────────────────────────────────────────────┐
│ Preload Context │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ const secret = 'SENSITIVE_DATA' │ │
│ │ const ipcRenderer = require('electron').ipcRenderer │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ contextBridge (只读暴露) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ window.safeAPI = { │ │
│ │ sendMessage: (msg) => ipcRenderer.send(msg) │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│ 隔离边界(无法穿越)
▼
┌─────────────────────────────────────────────────────────────┐
│ Renderer Context │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 无法访问 secret 或 ipcRenderer │ │
│ │ window.safeAPI.sendMessage('hello') // ✓ 可用 │ │
│ │ console.log(secret) // ✗ undefined │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘状态:默认禁用
必须禁用:
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false // 默认为 false,必须保持
}
})// ❌ 极度危险配置
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true, // 允许渲染进程访问 Node.js
contextIsolation: false, // 无上下文隔离
webSecurity: false // 禁用同源策略
}
})
win.loadURL('https://untrusted-site.com')
// 后果:远程网站可执行任意系统命令
// <script>require('child_process').exec('rm -rf /')</script>| 风险 | 说明 |
|---|---|
| 远程代码执行 | 加载远程内容时可执行任意 Node.js 代码 |
| 文件系统访问 | 可读写任意本地文件 |
| 系统命令执行 | 可执行 shell 命令 |
| 进程管理 | 可创建子进程 |
状态:从 Electron v20 开始默认启用
推荐启用:
const win = new BrowserWindow({
webPreferences: {
sandbox: true // 启用沙箱
}
})┌─────────────────────────────────────────────────────────────┐
│ Sandbox 环境 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✗ 无文件系统访问 (fs) │ │
│ │ ✗ 无子进程创建 (child_process) │ │
│ │ ✗ 无原生模块加载 │ │
│ │ ✗ 无系统信息获取 (os) │ │
│ │ ✓ 仅可通过 IPC 与主进程通信 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘// preload.js (sandbox: true 时)
const { contextBridge, ipcRenderer } = require('electron')
// ✅ 可用:ipcRenderer
// ✅ 可用:contextBridge
// ✅ 可用:process 对象(有限)
// ❌ 不可用:fs、child_process、原生模块
contextBridge.exposeInMainWorld('electronAPI', {
readFile: (path) => ipcRenderer.invoke('file:read', path)
})状态:默认启用
不要禁用:
const win = new BrowserWindow({
webPreferences: {
webSecurity: true // 默认为 true,不要禁用
}
})// ❌ 禁用 webSecurity 后
// 1. 同源策略失效
// 2. 可跨域读取任意网站数据
// 3. 可加载混合内容(HTTP + HTTPS)
// 4. 易受 CSRF 攻击// ✅ 使用主进程代理跨域请求
// main.js
ipcMain.handle('api:fetch', async (event, url, options) => {
const response = await fetch(url, options)
return response.json()
})
// preload.js
contextBridge.exposeInMainWorld('api', {
fetch: (url, options) => ipcRenderer.invoke('api:fetch', url, options)
})const { app, BrowserWindow } = require('electron')
const path = require('path')
function createWindow() {
const win = new BrowserWindow({
width: 800,
height: 600,
webPreferences: {
// === 安全配置(必须)===
contextIsolation: true, // 上下文隔离
nodeIntegration: false, // 禁用 Node.js 集成
webSecurity: true, // 启用 Web 安全
allowRunningInsecureContent: false, // 禁止混合内容
// 注:enableRemoteModule 已于 Electron 14 移除,无需再配置
// === 安全配置(推荐)===
sandbox: true, // 启用沙箱
nodeIntegrationInWorker: false, // 禁用 Worker Node 集成
nodeIntegrationInSubFrames: false, // 禁用子框架 Node 集成
// === 预加载脚本 ===
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
}
app.whenReady().then(createWindow)// 加载本地 HTML,无远程内容
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')// 加载可信的远程服务
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
// 不设置 preload,或仅暴露有限 API
}
})
win.loadURL('https://trusted-app.example.com')// 开发时可能需要额外配置
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, 'preload.js')
}
})
// 仅开发环境启用 DevTools
if (process.env.NODE_ENV === 'development') {
win.webContents.openDevTools()
}┌──────────────┐ ┌──────────────┐
│ Renderer │ │ Main │
│ │ ipcRenderer.invoke() │ │
│ │ ──────────────────────► │ │
│ │ ipcMain.handle() │ │
│ │ ◄────────────────────── │ │
│ │ 返回结果 │ │
└──────────────┘ └──────────────┘// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 定义允许的通道白名单
const ALLOWED_CHANNELS = {
invoke: ['dialog:open', 'file:read', 'file:write', 'app:getVersion'],
send: ['window:minimize', 'window:maximize', 'window:close'],
on: ['update:available', 'update:progress', 'notification:show']
}
// 验证通道是否在白名单中
function validateChannel(type, channel) {
if (!ALLOWED_CHANNELS[type]?.includes(channel)) {
console.error(`Blocked invalid IPC channel: ${channel}`)
return false
}
return true
}
// 安全暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
// 双向通信(返回 Promise)
invoke: (channel, ...args) => {
if (!validateChannel('invoke', channel)) {
return Promise.reject(new Error(`Invalid channel: ${channel}`))
}
return ipcRenderer.invoke(channel, ...args)
},
// 单向发送
send: (channel, ...args) => {
if (!validateChannel('send', channel)) {
return
}
ipcRenderer.send(channel, ...args)
},
// 监听事件
on: (channel, callback) => {
if (!validateChannel('on', channel)) {
return () => {}
}
const subscription = (event, ...args) => callback(...args)
ipcRenderer.on(channel, subscription)
// 返回取消订阅函数
return () => ipcRenderer.removeListener(channel, subscription)
},
// 一次性监听
once: (channel, callback) => {
if (!validateChannel('on', channel)) {
return
}
ipcRenderer.once(channel, (event, ...args) => callback(...args))
}
})// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
// 允许访问的基础目录
const ALLOWED_BASE_PATHS = [
app.getPath('documents'),
app.getPath('downloads'),
app.getPath('userData')
]
// 验证发送者
function isValidSender(webContentsId) {
// 实现你的验证逻辑,例如检查是否为已知窗口
return true
}
// 验证路径安全性
function isPathSafe(requestedPath, basePaths = ALLOWED_BASE_PATHS) {
const normalizedPath = path.normalize(requestedPath)
return basePaths.some(basePath =>
normalizedPath.startsWith(path.normalize(basePath))
)
}
// 文件读取处理器
ipcMain.handle('file:read', async (event, filePath) => {
// 1. 验证来源
if (!isValidSender(event.sender.id)) {
throw new Error('Unauthorized sender')
}
// 2. 验证参数类型
if (typeof filePath !== 'string' || filePath.length === 0) {
throw new TypeError('filePath must be a non-empty string')
}
// 3. 路径安全检查
const resolvedPath = path.resolve(filePath)
if (!isPathSafe(resolvedPath)) {
throw new Error(`Access denied: path outside allowed directories`)
}
// 4. 检查文件是否存在
if (!fs.existsSync(resolvedPath)) {
throw new Error(`File not found: ${resolvedPath}`)
}
// 5. 执行操作
try {
const content = await fs.promises.readFile(resolvedPath, 'utf-8')
return { success: true, content }
} catch (error) {
throw new Error(`Failed to read file: ${error.message}`)
}
})
// 文件写入处理器
ipcMain.handle('file:write', async (event, filePath, content) => {
// 参数验证
if (typeof filePath !== 'string') {
throw new TypeError('filePath must be a string')
}
if (typeof content !== 'string') {
throw new TypeError('content must be a string')
}
// 路径验证
const resolvedPath = path.resolve(filePath)
if (!isPathSafe(resolvedPath)) {
throw new Error('Access denied: path outside allowed directories')
}
// 写入文件
await fs.promises.writeFile(resolvedPath, content, 'utf-8')
return { success: true }
})
// 对话框处理器
ipcMain.handle('dialog:open', async (event, options = {}) => {
const result = await dialog.showOpenDialog({
title: options.title || '选择文件',
defaultPath: options.defaultPath,
filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
properties: ['openFile', ...(options.multiSelections ? ['multiSelections'] : [])]
})
// 返回选择的文件路径(已经是绝对路径)
return result.filePaths
})// renderer.js
async function readFile() {
try {
const result = await window.electronAPI.invoke('file:read', '/path/to/file.txt')
console.log('File content:', result.content)
} catch (error) {
console.error('Failed to read file:', error.message)
}
}
// 监听事件(返回取消订阅函数)
const unsubscribe = window.electronAPI.on('update:available', (info) => {
console.log('Update available:', info)
})
// 取消监听
// unsubscribe()内容安全策略(Content Security Policy)是一层额外的安全保护,用于限制资源加载来源,防止 XSS 和数据注入攻击。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
font-src 'self' data:;
">
<title>My Electron App</title>
</head>
<body>
<!-- 内容 -->
</body>
</html>const { session } = require('electron')
// 为默认会话设置 CSP
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: https:; " +
"connect-src 'self' https://api.example.com;"
]
}
})
})| 指令 | 说明 | 示例 |
|---|---|---|
default-src | 默认资源来源 | 'self' 仅限同源 |
script-src | JavaScript 来源 | 'self' 不允许内联脚本 |
style-src | CSS 来源 | 'self' 'unsafe-inline' |
img-src | 图片来源 | 'self' data: https: |
connect-src | 网络请求目标 | 'self' https://api.example.com |
font-src | 字体来源 | 'self' data: |
media-src | 媒体来源 | 'self' |
object-src | 插件来源 | 'none' 禁止插件 |
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
font-src 'self';
"><meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' ws://localhost:* http://localhost:*;
"><meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
img-src 'self' data: https:;
">// ❌ 极度危险:加载远程内容且启用 Node.js
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: true,
contextIsolation: false
}
})
win.loadURL('https://untrusted-site.com')
// 攻击者可在网页中执行:require('child_process').exec('malware')// ✅ 安全:加载本地内容
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, 'preload.js')
}
})
win.loadFile('index.html')
// ✅ 如必须加载远程内容,使用严格限制
const remoteWin = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
// 不设置 preload,或仅暴露非常有限的 API
}
})
remoteWin.loadURL('https://trusted-site.com')const { app, shell } = require('electron')
// 全局导航控制
app.on('web-contents-created', (event, contents) => {
// 阻止导航到非白名单 URL
contents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl)
// 白名单检查
const allowedOrigins = [
'https://myapp.com',
'https://api.myapp.com'
]
if (!allowedOrigins.includes(parsedUrl.origin)) {
console.warn(`Blocked navigation to: ${navigationUrl}`)
event.preventDefault()
}
})
// 控制新窗口打开
contents.setWindowOpenHandler(({ url }) => {
// 在默认浏览器中打开外部链接
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url)
}
// 阻止 Electron 创建新窗口
return { action: 'deny' }
})
// 阻止 iframe 嵌入不可信内容
contents.on('will-attach-webview', (event, webPreferences, params) => {
// 移除 preload 路径
delete webPreferences.preload
// 禁用 Node.js
webPreferences.nodeIntegration = false
webPreferences.contextIsolation = true
})
})// ❌ 危险:直接插入用户内容
document.innerHTML = userInput
element.insertAdjacentHTML('beforeend', userInput)
// ✅ 安全:使用 textContent
element.textContent = userInput
// ✅ 安全:使用 DOM API
const div = document.createElement('div')
div.textContent = userInput
container.appendChild(div)textContent 而非 innerHTMLcontextIsolationnodeIntegration// ❌ 危险:可被污染原型
const obj = {}
obj.__proto__.polluted = true // 污染 Object.prototype
// ✅ 安全:使用 Object.create(null)
const obj = Object.create(null)
// ✅ 安全:使用 Map 代替普通对象
const map = new Map()
map.set('key', 'value')// 在 preload.js 中,contextIsolation 已提供保护
// 但仍需注意以下情况:
// ❌ 危险:直接合并用户输入
Object.assign(target, userInput)
// ✅ 安全:使用 Object.defineProperty
Object.defineProperty(target, key, {
value: userInput[key],
writable: true,
configurable: true,
enumerable: true
})
// ✅ 安全:使用深度克隆
function safeClone(obj) {
return JSON.parse(JSON.stringify(obj))
}// ❌ 危险:未验证路径
const content = fs.readFileSync(userInput, 'utf-8')
// userInput 可能是 "../../../../etc/passwd"
// ✅ 安全:验证路径
const path = require('path')
function safeReadFile(userInput, baseDir) {
// 解析为绝对路径
const resolvedPath = path.resolve(baseDir, userInput)
// 确保路径在允许的目录内
if (!resolvedPath.startsWith(path.resolve(baseDir))) {
throw new Error('Access denied: path outside base directory')
}
return fs.readFileSync(resolvedPath, 'utf-8')
}// ❌ 危险:执行动态代码
eval(userInput)
new Function(userInput)()
setTimeout(userInput, 0)
setInterval(userInput, 0)
// ✅ 安全:避免执行动态代码
// 使用预定义的函数映射
const actions = {
'save': saveFile,
'load': loadFile,
'delete': deleteFile
}
function executeAction(actionName, ...args) {
const action = actions[actionName]
if (typeof action === 'function') {
return action(...args)
}
throw new Error(`Unknown action: ${actionName}`)
}// ❌ 危险:加载 HTTP 内容
win.loadURL('http://example.com')
// ❌ 危险:禁用证书验证
app.commandLine.appendSwitch('ignore-certificate-errors')
// ✅ 安全:仅加载 HTTPS 内容
win.loadURL('https://example.com')
// ✅ 安全:生产环境不要放行证书错误,交由系统证书链校验
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
// 任何基于证书字段(如 issuerName)的自定义放行都可被伪造,
// 生产环境应始终拒绝证书错误
event.preventDefault()
callback(false)
})| 检查项 | 说明 | 验证方式 |
|---|---|---|
contextIsolation: true | 上下文隔离 | 检查 webPreferences |
nodeIntegration: false | 禁用 Node.js 集成 | 检查 webPreferences |
webSecurity: true | 启用 Web 安全 | 检查 webPreferences |
使用 contextBridge | 安全暴露 API | 检查 preload 脚本 |
| IPC 通道白名单 | 限制可用的 IPC 通道 | 检查 preload 脚本 |
| 设置 CSP | 内容安全策略 | 检查 HTML 或主进程 |
| 检查项 | 说明 |
|---|---|
sandbox: true | 启用沙箱模式 |
| 验证 IPC 参数 | 主进程验证所有输入 |
| 限制文件访问路径 | 使用白名单目录 |
| 导航验证 | 监听 will-navigate |
| 新窗口控制 | 使用 setWindowOpenHandler |
| 检查项 | 风险 |
|---|---|
❌ contextIsolation: false | 原型污染、API 泄露 |
❌ nodeIntegration: true | 远程代码执行 |
❌ webSecurity: false | 同源策略失效 |
❌ 暴露整个 ipcRenderer | 权限过大 |
| ❌ 加载不信任的远程内容 | 远程代码执行 |
nodeIntegration 后,渲染进程无法使用 require?A: 这是安全设计的预期行为。渲染进程不应直接访问 Node.js API。正确做法是通过 preload 脚本和 contextBridge 暴露需要的 API:
// preload.js
const { contextBridge } = require('electron')
const fs = require('fs')
contextBridge.exposeInMainWorld('nodeAPI', {
readFile: (path) => fs.readFileSync(path, 'utf-8')
})sandbox: true 和 sandbox: false 有什么区别?A:
| 特性 | sandbox: false | sandbox: true |
|---|---|---|
| Preload 脚本 Node.js 访问 | 完整 | 受限 |
| 原生模块 | 可用 | 不可用 |
| 安全性 | 较低 | 更高 |
推荐启用沙箱,需要在主进程处理文件系统等操作。
A:
// 创建隔离的 BrowserView 或 BrowserWindow
const win = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
// 不设置 preload
}
})
// 设置导航限制
win.webContents.on('will-navigate', (event) => {
event.preventDefault()
})
win.loadURL('https://trusted-site.com')A: 检查浏览器控制台的 CSP 错误信息,逐步放宽限制:
<!-- 调试时可以先使用宽松策略 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' 'unsafe-inline' 'unsafe-eval';
connect-src 'self' *;
">
<!-- 生产环境逐步收紧 -->A: 使用以下工具和方法:
npx @doyensec/electronegativity -i ./appBrowserWindow 配置contextBridge 暴露的 API 可以被篡改吗?A: 不可以。contextBridge 暴露的值是只读的,渲染进程无法修改:
// preload.js
contextBridge.exposeInMainWorld('api', {
secret: 'value'
})
// renderer.js
window.api.secret = 'hacked' // 静默失败,值不变
console.log(window.api.secret) // 仍然是 'value'
delete window.api.secret // 静默失败本文档从应用安全性和源码安全性两个维度,详细介绍 Electron 应用的安全最佳实践。遵循这些指南可以有效防范常见安全威胁,保护应用和用户数据安全。
Electron 通过将 Chromium 和 Node.js 结合在一起,使得开发者能用 Web 技术构建桌面应用。然而,这种结合也带来了独特的安全挑战:
┌─────────────────────────────────────────────────────────────┐
│ Electron 安全风险矩阵 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Chromium (渲染进程) Node.js (主进程) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ • XSS 攻击 │ IPC │ • 文件系统访问 │ │
│ │ • 恶意网页 │ ──────► │ • 系统命令执行 │ │
│ │ • 远程代码注入 │ │ • 进程管理 │ │
│ │ • 数据泄露 │ │ • 网络请求 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 风险:渲染进程被攻破 → 通过 Node.js 获得系统控制权 │
│ │
└─────────────────────────────────────────────────────────────┘核心风险:如果渲染进程被恶意代码利用,可能通过 Node.js 的能力访问操作系统,导致严重的安全漏洞,如:
| 漏洞类型 | 危害等级 | 潜在后果 |
|---|---|---|
| 远程代码执行(RCE) | 严重 | 攻击者可执行任意系统命令 |
| 本地文件读取 | 高 | 敏感数据泄露 |
| 路径遍历 | 高 | 访问预期外的文件 |
| 原型污染 | 中高 | 绕过安全检查 |
应用安全的核心思想是权限最小化,即限制渲染进程访问 Node.js 和操作系统资源的能力。
contextIsolation 是 Electron 最重要的安全特性之一。它确保预加载脚本(preload.js)和渲染器进程的 Web 内容运行在不同的 JavaScript 上下文中。
| Electron 版本 | 默认值 |
|---|---|
| < v12 | false |
| >= v12 | true |
// main.js
const mainWindow = new BrowserWindow({
webPreferences: {
// preload 是必须的,用于连接主进程和渲染进程
preload: path.join(app.getAppPath(), 'preload.js')
// contextIsolation 默认为 true,无需显式设置
// contextIsolation: true,
}
})// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// ✅ 正确:通过 contextBridge 安全地暴露 API
contextBridge.exposeInMainWorld('myAPI', {
// 暴露特定方法
doAThing: () => ipcRenderer.send('do-a-thing'),
// 双向通信(返回 Promise)
loadPreferences: () => ipcRenderer.invoke('load-prefs'),
// 带参数的方法
saveFile: (content) => ipcRenderer.invoke('file:save', content),
// 事件监听
onUpdate: (callback) => {
ipcRenderer.on('update-available', (event, info) => callback(info))
}
})
// ❌ 错误:直接暴露整个 ipcRenderer(危险!)
// contextBridge.exposeInMainWorld('ipc', ipcRenderer)// renderer.js
// 只能调用暴露的方法,无法访问 Node.js 或 Electron 内部 API
async function init() {
const prefs = await window.myAPI.loadPreferences()
console.log('Preferences:', prefs)
}
window.myAPI.onUpdate((info) => {
console.log('Update available:', info)
})┌─────────────────────────────────────────────────────────────┐
│ contextIsolation: false (不安全) │
├─────────────────────────────────────────────────────────────┤
│ Preload Context (共享) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ window.secret = 'sensitive-data' │ │
│ │ window.electron = require('electron') │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ 可互相访问 │
│ ▼ │
│ Renderer Context │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ // 恶意代码可以: │ │
│ │ console.log(window.secret) // 读取敏感数据 │ │
│ │ window.electron.ipcRenderer.send(...) // 完全控制 │ │
│ │ window.__proto__.polluted = true // 原型污染 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ contextIsolation: true (安全) │
├─────────────────────────────────────────────────────────────┤
│ Preload Context (隔离) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ const secret = 'sensitive-data' │ │
│ │ const electron = require('electron') │ │
│ │ // 通过 contextBridge 暴露有限 API │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ 只读代理 │
│ ▼ │
│ Renderer Context │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ window.myAPI.saveFile() // ✓ 只能用暴露的 API │ │
│ │ console.log(secret) // ✗ undefined │ │
│ │ window.myAPI = hacked // ✗ 无法修改 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘沙箱化进一步隔离渲染进程,使其在一个受限的环境中运行,即使在 contextIsolation 被绕过的情况下,也能限制其对操作系统的访问。
| Electron 版本 | 默认值 |
|---|---|
| < v20 | false |
| >= v20 | true |
// main.js
const mainWindow = new BrowserWindow({
webPreferences: {
sandbox: true, // 默认为 true(Electron v20+)
preload: path.join(app.getAppPath(), 'preload.js')
}
})// preload.js (sandbox: true 时)
// ✅ 可用
const { contextBridge, ipcRenderer } = require('electron')
// ✅ 可用:有限的 process 对象
console.log(process.platform) // 'darwin', 'win32', etc.
console.log(process.type) // 'renderer'
// ❌ 不可用:文件系统
// const fs = require('fs') // Error
// ❌ 不可用:子进程
// const { exec } = require('child_process') // Error
// ❌ 不可用:原生模块
// const native = require('./native.node') // Error
// 正确做法:通过 IPC 让主进程处理
contextBridge.exposeInMainWorld('fs', {
readFile: (path) => ipcRenderer.invoke('fs:read', path),
writeFile: (path, content) => ipcRenderer.invoke('fs:write', path, content)
})┌─────────────────────────────────────────────────────────────┐
│ 沙箱进程 (sandbox: true) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Chromium 沙箱 │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ 渲染进程 │ │ │
│ │ │ • 无文件系统访问 │ │ │
│ │ │ • 无网络原始套接字 │ │ │
│ │ │ • 无进程创建能力 │ │ │
│ │ │ • 受限的系统调用 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ │ IPC (受限) │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 主进程 │ │
│ │ • 完整 Node.js 能力 │ │
│ │ • 处理文件、网络、进程等操作 │ │
│ │ • 验证所有来自沙箱的请求 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘注意:当
sandbox: true时,预加载脚本中的 Node.js 能力受限。所有文件系统、网络等操作需通过 IPC 由主进程处理。
nodeIntegration 允许渲染进程直接访问 Node.js API。这是一个非常危险的设置,应始终禁用。
| Electron 版本 | 默认值 |
|---|---|
| 所有版本 | false |
// ❌ 极度危险配置!
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: true, // 允许渲染进程访问 Node.js
contextIsolation: false, // 无上下文隔离
webSecurity: false // 禁用同源策略
}
})
mainWindow.loadURL('https://example.com')
// 如果 example.com 被攻破或包含恶意脚本:
// <script>
// const { exec } = require('child_process')
// exec('rm -rf ~/Documents') // 删除用户文件
// const fs = require('fs')
// const password = fs.readFileSync('/etc/passwd') // 读取系统文件
// </script>// ✅ 安全配置
const mainWindow = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 保持默认
contextIsolation: true, // 启用上下文隔离
sandbox: true, // 启用沙箱
preload: path.join(__dirname, 'preload.js')
}
})| 如果启用 | 后果 |
|---|---|
可访问 fs 模块 | 读写任意文件 |
可访问 child_process | 执行任意系统命令 |
可访问 net 模块 | 绕过防火墙 |
可访问 os 模块 | 获取系统信息 |
| 加载远程内容时 | 远程代码执行(RCE) |
webSecurity 强制执行同源策略,阻止跨站脚本(XSS)攻击。
| Electron 版本 | 默认值 |
|---|---|
| 所有版本 | true |
// ❌ 禁用 webSecurity 会带来严重风险
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false // 危险!
}
})
// 后果:
// 1. 同源策略失效
// 2. 可跨域读取任意网站数据
// 3. 可加载混合内容(HTTP + HTTPS)
// 4. 易受 CSRF 攻击// ✅ 使用主进程代理跨域请求
// main.js
const { ipcMain } = require('electron')
ipcMain.handle('http:fetch', async (event, url, options) => {
// 验证 URL
const allowedDomains = ['api.example.com', 'cdn.example.com']
const urlObj = new URL(url)
if (!allowedDomains.includes(urlObj.hostname)) {
throw new Error('Domain not allowed')
}
const response = await fetch(url, options)
return {
ok: response.ok,
status: response.status,
data: await response.json()
}
})
// preload.js
contextBridge.exposeInMainWorld('http', {
fetch: (url, options) => ipcRenderer.invoke('http:fetch', url, options)
})
// renderer.js
const data = await window.http.fetch('https://api.example.com/data')// main.js - 设置 CSP 和 CORS
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ["default-src 'self'"],
'Access-Control-Allow-Origin': ['https://trusted-domain.com']
}
})
})防止应用意外导航到恶意网站或打开非预期的窗口。
// main.js
const { app } = require('electron')
// 全局导航控制
app.on('web-contents-created', (event, contents) => {
// 阻止所有非白名单的导航
contents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl)
// 定义允许的源
const allowedOrigins = [
'https://myapp.com',
'https://docs.myapp.com'
]
if (!allowedOrigins.includes(parsedUrl.origin)) {
console.warn(`Blocked navigation to: ${navigationUrl}`)
event.preventDefault()
}
})
// 防止页面通过 JavaScript 跳转
contents.on('will-redirect', (event, url) => {
// 应用相同的安全策略
const parsedUrl = new URL(url)
if (parsedUrl.origin !== 'https://myapp.com') {
event.preventDefault()
}
})
})// main.js
const { shell } = require('electron')
app.on('web-contents-created', (event, contents) => {
// 控制新窗口打开行为
contents.setWindowOpenHandler(({ url, frameName, disposition }) => {
// 外部链接在默认浏览器中打开
if (url.startsWith('http://') || url.startsWith('https://')) {
shell.openExternal(url)
return { action: 'deny' }
}
// 特定的内部页面可以打开新窗口
if (url.startsWith('app://')) {
return {
action: 'allow',
overrideBrowserWindowOptions: {
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true
}
}
}
}
// 其他请求拒绝
return { action: 'deny' }
})
})// 阻止 iframe 加载不可信内容
contents.on('will-attach-webview', (event, webPreferences, params) => {
// 移除 preload
delete webPreferences.preload
// 禁用 Node.js
webPreferences.nodeIntegration = false
webPreferences.contextIsolation = true
// 验证 src
if (!params.src.startsWith('https://trusted-domain.com')) {
event.preventDefault()
}
})所有从渲染进程到主进程的 IPC 消息都应被视为不可信的,必须进行严格验证。
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
// 定义允许的通道
const ALLOWED_CHANNELS = {
invoke: ['app:version', 'file:read', 'file:write', 'dialog:open'],
send: ['window:close', 'window:minimize', 'window:maximize'],
receive: ['update:available', 'update:downloaded', 'notification:show']
}
function checkChannel(type, channel) {
if (!ALLOWED_CHANNELS[type]?.includes(channel)) {
console.error(`IPC channel "${channel}" not in ${type} whitelist`)
return false
}
return true
}
contextBridge.exposeInMainWorld('electron', {
invoke: (channel, ...args) => {
if (!checkChannel('invoke', channel)) {
return Promise.reject(new Error('Channel not allowed'))
}
return ipcRenderer.invoke(channel, ...args)
},
send: (channel, ...args) => {
if (!checkChannel('send', channel)) {
return
}
ipcRenderer.send(channel, ...args)
},
on: (channel, callback) => {
if (!checkChannel('receive', channel)) {
return () => {}
}
const handler = (event, ...args) => callback(...args)
ipcRenderer.on(channel, handler)
return () => ipcRenderer.removeListener(channel, handler)
}
})// main.js
const { ipcMain, app, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
const { z } = require('zod') // 可选:使用 zod 进行 schema 验证
// 定义允许的目录
const ALLOWED_DIRS = {
documents: app.getPath('documents'),
downloads: app.getPath('downloads'),
userData: app.getPath('userData')
}
// 验证路径
function validatePath(userPath, allowedDir) {
const resolved = path.resolve(allowedDir, userPath)
if (!resolved.startsWith(allowedDir)) {
throw new Error('Path traversal attempt blocked')
}
return resolved
}
// 使用 schema 验证(推荐)
const readFileSchema = z.object({
path: z.string().min(1).max(1024),
encoding: z.enum(['utf-8', 'binary', 'base64']).optional()
})
ipcMain.handle('file:read', async (event, params) => {
// 1. Schema 验证
const validated = readFileSchema.parse(params)
// 2. 路径安全检查
const safePath = validatePath(validated.path, ALLOWED_DIRS.userData)
// 3. 执行操作
const content = await fs.promises.readFile(safePath, validated.encoding || 'utf-8')
return content
})
// 手动验证示例
ipcMain.handle('dialog:open', async (event, options) => {
// 验证参数类型
if (options && typeof options !== 'object') {
throw new TypeError('options must be an object')
}
// 验证具体字段
if (options?.title && typeof options.title !== 'string') {
throw new TypeError('title must be a string')
}
// 使用安全的默认值
const result = await dialog.showOpenDialog({
title: options?.title || '选择文件',
defaultPath: ALLOWED_DIRS.documents,
filters: options?.filters || [{ name: 'All Files', extensions: ['*'] }],
properties: ['openFile']
})
return result.filePaths
})┌─────────────────────────────────────────────────────────────┐
│ IPC 安全验证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 渲染进程 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ window.electron.invoke('file:read', { path: '...' })│ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Preload 脚本 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 检查通道白名单 │ │
│ │ 2. 检查参数类型(基础) │ │
│ │ 3. 转发到主进程 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 主进程 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 验证发送者身份 │ │
│ │ 2. Schema/类型验证 │ │
│ │ 3. 路径/URL 安全检查 │ │
│ │ 4. 权限检查 │ │
│ │ 5. 执行操作 │ │
│ │ 6. 返回结果(过滤敏感信息) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘内容安全策略(CSP)是防止 XSS 攻击的重要防线。
方式一:HTML Meta 标签
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.example.com;
">
</head>
<body>
<!-- 应用内容 -->
</body>
</html>方式二:主进程设置
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'; " +
"connect-src 'self' https://api.example.com;"
]
}
})
})<!-- 严格模式:本地应用 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self';
style-src 'self';
img-src 'self' data:;
connect-src 'self';
">
<!-- 开发模式 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
connect-src 'self' ws://localhost:* http://localhost:*;
">
<!-- 允许 CDN -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self';
script-src 'self' https://cdn.jsdelivr.net;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
">源码安全的目标是增加攻击者逆向工程和篡改代码的难度。以下是几种常见的源码保护策略:
默认情况下,Electron 将应用源码打包成 app.asar 文件。这是一个简单的归档文件,可以被轻松解压。
# 攻击者可以轻松解压 asar
npx asar extract app.asar ./extracted
# 解压后可修改代码(例如绕过授权检查)
# 然后重新打包
npx asar pack ./extracted app.asar方式一:asar 完整性校验
# electron-builder.yml
asar: true
asarUnpack:
- "**/*.node" # 原生模块不打包进 asarelectron-builder 23+ 会在打包时自动为 asar 生成完整性校验信息(macOS 随代码签名启用,Windows 在 electron-builder 24+ 默认开启),Electron 运行时会自动校验 asar 是否被篡改,无需额外配置项。
方式二:启动时校验
// main.js
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')
function verifyIntegrity() {
const expectedHash = 'your-expected-sha256-hash'
const asarPath = path.join(process.resourcesPath, 'app.asar')
if (fs.existsSync(asarPath)) {
const content = fs.readFileSync(asarPath)
const hash = crypto.createHash('sha256').update(content).digest('hex')
if (hash !== expectedHash) {
dialog.showErrorBox('安全警告', '应用文件已被篡改')
app.quit()
}
}
}
app.on('ready', verifyIntegrity)压缩和混淆可以减小代码体积,并极大地增加逆向工程的难度。
// vite.config.js 或 webpack.config.js
import { defineConfig } from 'vite'
import terser from '@rollup/plugin-terser'
export default defineConfig({
build: {
minify: 'terser',
terserOptions: {
compress: {
drop_console: true, // 移除 console
drop_debugger: true, // 移除 debugger
pure_funcs: ['console.log']
},
mangle: {
properties: {
regex: /^_/ // 混淆以 _ 开头的属性
}
}
}
}
})// vite.config.js
import obfuscator from 'rollup-plugin-obfuscator'
export default defineConfig({
plugins: [
obfuscator({
options: {
compact: true,
controlFlowFlattening: true, // 控制流平坦化
deadCodeInjection: true, // 注入死代码
debugProtection: true, // 禁用调试
disableConsoleOutput: true, // 禁用控制台
stringArray: true, // 字符串数组化
stringArrayEncoding: ['base64'], // 字符串编码
transformObjectKeys: true // 转换对象键
}
})
]
})┌─────────────────────────────────────────────────────────────┐
│ 混淆级别与性能权衡 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 低混淆 ◄────────────────────────────────────► 高混淆 │
│ │
│ • 启动快 • 启动慢 │
│ • 体积小 • 体积大 │
│ • 易逆向 • 难逆向 │
│ • 调试方便 • 调试困难 │
│ │
│ 推荐:仅对核心敏感代码使用高混淆 │
│ │
└─────────────────────────────────────────────────────────────┘对于最核心的算法或商业逻辑,可以将其用 C++ 或 Rust 编写成 Node.js 原生模块。
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ C++ / Rust │ ──►│ 编译工具 │ ──►│ .node 文件 │
│ 源代码 │ │ node-gyp │ │ (二进制) │
└──────────────┘ │ N-API │ └──────────────┘
│ Neon │ │
└──────────────┘ │
▼
┌──────────────┐ ┌──────────────┐
│ JavaScript │ ◄──│ 加载模块 │
│ 应用代码 │ │ require() │
└──────────────┘ └──────────────┘// native.cc
#include <node_api.h>
#include <string>
// 核心加密逻辑
std::string encryptData(const std::string& data, const std::string& key) {
// 实现加密算法
// ...
return encrypted;
}
// Node.js 绑定
static napi_value Encrypt(napi_env env, napi_callback_info info) {
size_t argc = 2;
napi_value args[2];
napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
// 获取参数...
// 调用加密函数...
// 返回结果...
return result;
}
static napi_value Init(napi_env env, napi_value exports) {
napi_property_descriptor desc = { "encrypt", nullptr, Encrypt, nullptr, nullptr, nullptr, napi_default, nullptr };
napi_define_properties(env, exports, 1, &desc);
return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)// 使用原生模块
const native = require('./build/Release/native.node')
const encrypted = native.encrypt('sensitive-data', 'secret-key')// src/lib.rs
use neon::prelude::*;
fn encrypt(mut cx: FunctionContext) -> JsResult<JsString> {
let data = cx.argument::<JsString>(0)?.value(&mut cx);
let key = cx.argument::<JsString>(1)?.value(&mut cx);
// 实现加密逻辑
let encrypted = format!("encrypted:{}:{}", key, data);
Ok(cx.string(encrypted))
}
#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
cx.export_function("encrypt", encrypt)?;
Ok(())
}V8 字节码将 JavaScript 源码编译成 V8 引擎可直接执行的字节码格式,应用中不包含可读的源码。
// electron.vite.config.ts
import { defineConfig, bytecodePlugin } from 'electron-vite'
export default defineConfig({
main: {
plugins: [bytecodePlugin({
// 字节码配置
chunkAlias: 'main' // 指定需要编译的 chunk
})]
},
preload: {
plugins: [bytecodePlugin()]
},
renderer: {
// 渲染进程通常不需要字节码
// 因为代码可能需要动态执行
}
})// 编译脚本 compile.js
const bytenode = require('bytenode')
const path = require('path')
async function compile() {
// 编译主进程代码
await bytenode.compileFile({
filename: path.join(__dirname, 'main.js'),
output: path.join(__dirname, 'main.jsc')
})
// 创建入口文件
const entry = `require('bytenode');
require('./main.jsc');`
require('fs').writeFileSync(path.join(__dirname, 'entry.js'), entry)
}
compile()| 特性 | 说明 |
|---|---|
| 无源码 | 最终交付不包含可读 JavaScript |
| 启动快 | 省去 V8 解析编译步骤 |
| 难逆向 | 字节码比混淆代码更难分析 |
// ⚠️ 字节码不支持的功能
// 1. 动态代码执行
eval(code) // ❌ 不支持
new Function(code) // ❌ 不支持
// 2. 动态导入(需要特殊处理)
import(moduleName) // ❌ 可能不支持
// 3. 跨版本兼容
// V8 字节码与 V8 版本绑定
// 不同 Electron 版本可能不兼容const secureConfig = {
// === 必须配置 ===
contextIsolation: true, // ✅ 必须启用
nodeIntegration: false, // ✅ 必须禁用
webSecurity: true, // ✅ 必须启用
allowRunningInsecureContent: false, // ✅ 必须禁用
// 注:enableRemoteModule 已于 Electron 14 移除,无需再配置
// === 推荐配置 ===
sandbox: true, // ✅ 推荐启用
nodeIntegrationInWorker: false, // ✅ 推荐禁用
nodeIntegrationInSubFrames: false, // ✅ 推荐禁用
// === 预加载脚本 ===
preload: path.join(__dirname, 'preload.js')
}// security-check.js
const { BrowserWindow } = require('electron')
function checkWindowSecurity(win) {
const prefs = win.webContents.getLastWebPreferences()
const issues = []
if (!prefs.contextIsolation) {
issues.push('❌ contextIsolation is disabled')
}
if (prefs.nodeIntegration) {
issues.push('❌ nodeIntegration is enabled')
}
if (!prefs.webSecurity) {
issues.push('❌ webSecurity is disabled')
}
if (prefs.enableRemoteModule) {
issues.push('❌ enableRemoteModule is enabled')
}
return issues.length === 0 ? '✅ All security checks passed' : issues
}
// 在创建窗口后调用
app.on('browser-window-created', (event, win) => {
console.log('Security check:', checkWindowSecurity(win))
})A: 通过 preload 脚本和 contextBridge 安全暴露 API:
// preload.js
const { contextBridge, ipcRenderer } = require('electron')
const fs = require('fs')
contextBridge.exposeInMainWorld('nodeAPI', {
readFile: (path) => fs.readFileSync(path, 'utf-8'),
writeFile: (path, content) => fs.writeFileSync(path, content)
})A: 文件操作必须通过 IPC 由主进程执行:
// preload.js
contextBridge.exposeInMainWorld('fs', {
read: (path) => ipcRenderer.invoke('fs:read', path)
})
// main.js
ipcMain.handle('fs:read', async (event, path) => {
// 验证路径...
return fs.readFileSync(path, 'utf-8')
})A: 开发环境不混淆,仅生产环境混淆:
// vite.config.js
export default defineConfig(({ mode }) => ({
plugins: mode === 'production' ? [obfuscator()] : []
}))A: 查看控制台错误,逐步放宽限制:
<!-- 调试时使用宽松策略 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' 'unsafe-inline' 'unsafe-eval' *;
">
<!-- 确定需要的资源后收紧策略 -->A: 不要在渲染进程存储敏感数据:
// ❌ 错误:在渲染进程存储
// localStorage.setItem('apiKey', 'secret-key')
// ✅ 正确:在主进程存储
// main.js
const keytar = require('keytar') // 注:keytar 已停止维护,新项目建议使用 Electron 内置的 safeStorage API
ipcMain.handle('api:getKey', async () => {
return keytar.getPassword('myapp', 'api-key')
})
ipcMain.handle('api:setKey', async (event, key) => {
return keytar.setPassword('myapp', 'api-key', key)
})A: 实现完整性校验:
const crypto = require('crypto')
const fs = require('fs')
function checkIntegrity() {
const knownHash = 'your-precomputed-hash'
const asarPath = `${process.resourcesPath}/app.asar`
if (fs.existsSync(asarPath)) {
const hash = crypto
.createHash('sha256')
.update(fs.readFileSync(asarPath))
.digest('hex')
if (hash !== knownHash) {
// 应用被篡改
app.quit()
}
}
}Electron 应用的安全性是一个系统工程,需要从应用层和源码层两个维度进行综合防护。开发者应始终保持警惕,遵循最新的安全最佳实践。
| 原则 | 说明 |
|---|---|
| 最小权限 | 渲染进程的权限越小越好 |
| 深度防御 | 同时采用多种安全策略 |
| 输入验证 | 所有来自渲染进程的数据都不可信 |
| 源码保护 | 增加逆向工程和代码篡改的成本 |
必须项:
- [x] contextIsolation: true
- [x] nodeIntegration: false
- [x] webSecurity: true
- [x] enableRemoteModule: false
- [x] 使用 contextBridge
- [x] IPC 通道白名单
- [x] 设置 CSP
推荐项:
- [ ] sandbox: true
- [ ] IPC 参数验证
- [ ] 路径安全检查
- [ ] 代码混淆
- [ ] asar 完整性校验(注:原清单中的 enableRemoteModule: false 已于 Electron 14 随远程模块一并移除,无需再检查。)