Electron 应用安全性指南
本文档从应用安全性和源码安全性两个维度,详细介绍 Electron 应用的安全最佳实践。遵循这些指南可以有效防范常见安全威胁,保护应用和用户数据安全。
前言:Electron 的安全挑战
Electron 通过将 Chromium 和 Node.js 结合在一起,使得开发者能用 Web 技术构建桌面应用。然而,这种结合也带来了独特的安全挑战:
┌─────────────────────────────────────────────────────────────┐
│ Electron 安全风险矩阵 │
├─────────────────────────────────────────────────────────────┤
│ │
│ Chromium (渲染进程) Node.js (主进程) │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ • XSS 攻击 │ IPC │ • 文件系统访问 │ │
│ │ • 恶意网页 │ ──────► │ • 系统命令执行 │ │
│ │ • 远程代码注入 │ │ • 进程管理 │ │
│ │ • 数据泄露 │ │ • 网络请求 │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ 风险:渲染进程被攻破 → 通过 Node.js 获得系统控制权 │
│ │
└─────────────────────────────────────────────────────────────┘核心风险:如果渲染进程被恶意代码利用,可能通过 Node.js 的能力访问操作系统,导致严重的安全漏洞,如:
| 漏洞类型 | 危害等级 | 潜在后果 |
|---|---|---|
| 远程代码执行(RCE) | 严重 | 攻击者可执行任意系统命令 |
| 本地文件读取 | 高 | 敏感数据泄露 |
| 路径遍历 | 高 | 访问预期外的文件 |
| 原型污染 | 中高 | 绕过安全检查 |
应用安全性
应用安全的核心思想是权限最小化,即限制渲染进程访问 Node.js 和操作系统资源的能力。
1. 开启 contextIsolation(上下文隔离)
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,
}
})预加载脚本安全暴露 API
// 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 // ✗ 无法修改 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘2. 开启 sandbox(沙箱模式)
沙箱化进一步隔离渲染进程,使其在一个受限的环境中运行,即使在 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 由主进程处理。
3. 禁用 nodeIntegration
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')
}
})禁用 nodeIntegration 的理由
| 如果启用 | 后果 |
|---|---|
可访问 fs 模块 | 读写任意文件 |
可访问 child_process | 执行任意系统命令 |
可访问 net 模块 | 绕过防火墙 |
可访问 os 模块 | 获取系统信息 |
| 加载远程内容时 | 远程代码执行(RCE) |
4. 确保 webSecurity 开启
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']
}
})
})5. 限制导航与新窗口创建
防止应用意外导航到恶意网站或打开非预期的窗口。
导航限制
// 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 安全控制
// 阻止 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()
}
})6. 校验与过滤 IPC 通信
所有从渲染进程到主进程的 IPC 消息都应被视为不可信的,必须进行严格验证。
Preload 脚本白名单
// 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 安全流程
┌─────────────────────────────────────────────────────────────┐
│ IPC 安全验证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 渲染进程 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ window.electron.invoke('file:read', { path: '...' })│ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Preload 脚本 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 检查通道白名单 │ │
│ │ 2. 检查参数类型(基础) │ │
│ │ 3. 转发到主进程 │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ 主进程 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 1. 验证发送者身份 │ │
│ │ 2. Schema/类型验证 │ │
│ │ 3. 路径/URL 安全检查 │ │
│ │ 4. 权限检查 │ │
│ │ 5. 执行操作 │ │
│ │ 6. 返回结果(过滤敏感信息) │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘7. 内容安全策略
内容安全策略(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;"
]
}
})
})常用 CSP 配置
<!-- 严格模式:本地应用 -->
<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;
">源码安全性
源码安全的目标是增加攻击者逆向工程和篡改代码的难度。以下是几种常见的源码保护策略:
1. asar 文件防护
默认情况下,Electron 将应用源码打包成 app.asar 文件。这是一个简单的归档文件,可以被轻松解压。
风险分析
# 攻击者可以轻松解压 asar
npx asar extract app.asar ./extracted
# 解压后可修改代码(例如绕过授权检查)
# 然后重新打包
npx asar pack ./extracted app.asar防护措施
方式一:asar 完整性校验
// electron-builder 配置
// electron-builder.yml
asar: true
asarUnpack:
- "**/*.node" # 原生模块不打包
// 启用 asar 完整性校验(需要 electron-builder 23+)
// package.json
{
"build": {
"asar": true,
"asarSign": {
"algorithm": "sha256"
}
}
}方式二:启动时校验
// 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)2. JavaScript 代码压缩与混淆
压缩和混淆可以减小代码体积,并极大地增加逆向工程的难度。
使用 Terser 压缩
// 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: /^_/ // 混淆以 _ 开头的属性
}
}
}
}
})使用 javascript-obfuscator 混淆
// 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 // 转换对象键
}
})
]
})性能注意事项
┌─────────────────────────────────────────────────────────────┐
│ 混淆级别与性能权衡 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 低混淆 ◄────────────────────────────────────► 高混淆 │
│ │
│ • 启动快 • 启动慢 │
│ • 体积小 • 体积大 │
│ • 易逆向 • 难逆向 │
│ • 调试方便 • 调试困难 │
│ │
│ 推荐:仅对核心敏感代码使用高混淆 │
│ │
└─────────────────────────────────────────────────────────────┘3. 核心逻辑原生模块加密
对于最核心的算法或商业逻辑,可以将其用 C++ 或 Rust 编写成 Node.js 原生模块。
开发流程
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ C++ / Rust │ ──►│ 编译工具 │ ──►│ .node 文件 │
│ 源代码 │ │ node-gyp │ │ (二进制) │
└──────────────┘ │ N-API │ └──────────────┘
│ Neon │ │
└──────────────┘ │
▼
┌──────────────┐ ┌──────────────┐
│ JavaScript │ ◄──│ 加载模块 │
│ 应用代码 │ │ require() │
└──────────────┘ └──────────────┘C++ 原生模块示例
// 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')Rust 原生模块示例(使用 Neon)
// 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(())
}4. 使用 V8 字节码
V8 字节码将 JavaScript 源码编译成 V8 引擎可直接执行的字节码格式,应用中不包含可读的源码。
使用 electron-vite
// electron.vite.config.ts
import { defineConfig, bytecodePlugin } from 'electron-vite'
export default defineConfig({
main: {
plugins: [bytecodePlugin({
// 字节码配置
chunkAlias: 'main' // 指定需要编译的 chunk
})]
},
preload: {
plugins: [bytecodePlugin()]
},
renderer: {
// 渲染进程通常不需要字节码
// 因为代码可能需要动态执行
}
})使用 bytenode
// 编译脚本 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 版本可能不兼容安全配置速查表
webPreferences 配置
const secureConfig = {
// === 必须配置 ===
contextIsolation: true, // ✅ 必须启用
nodeIntegration: false, // ✅ 必须禁用
webSecurity: true, // ✅ 必须启用
enableRemoteModule: false, // ✅ 必须禁用
allowRunningInsecureContent: false, // ✅ 必须禁用
// === 推荐配置 ===
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))
})常见问题解答
Q1: 如何在禁用 nodeIntegration 后访问 Node.js 功能?
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)
})Q2: sandbox 模式下如何进行文件操作?
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')
})Q3: 如何调试被混淆的代码?
A: 开发环境不混淆,仅生产环境混淆:
// vite.config.js
export default defineConfig(({ mode }) => ({
plugins: mode === 'production' ? [obfuscator()] : []
}))Q4: CSP 导致某些功能不工作怎么办?
A: 查看控制台错误,逐步放宽限制:
<!-- 调试时使用宽松策略 -->
<meta http-equiv="Content-Security-Policy" content="
default-src 'self' 'unsafe-inline' 'unsafe-eval' *;
">
<!-- 确定需要的资源后收紧策略 -->Q5: 如何保护敏感数据(如 API 密钥)?
A: 不要在渲染进程存储敏感数据:
// ❌ 错误:在渲染进程存储
// localStorage.setItem('apiKey', 'secret-key')
// ✅ 正确:在主进程存储
// main.js
const keytar = require('keytar')
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)
})Q6: 如何检测应用是否被篡改?
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 完整性校验延伸阅读
- 安全模型 - 深入理解 Electron 安全架构
- Electron 官方安全指南
- Electron 安全公告
- Electronegativity 安全审计工具
- OWASP Electron 安全指南