Electron 安全最佳实践
前言
Electron 应用的安全性是一个常被忽视但至关重要的话题。由于 Electron 本质上是一个浏览器内核,Web 安全的所有威胁(XSS、CSRF、注入攻击等)在 Electron 应用中都存在。此外,Electron 的进程架构和 Node.js 集成还引入了额外的攻击面。本文将全面讲解 Electron 应用的安全最佳实践,帮助你构建不易被攻击的桌面应用。
基础安全配置
核心配置组合
这是 Electron 应用最基本的安全配置,也是任何生产应用必须满足的底线:
// main.ts — 必需的安全配置
const win = new BrowserWindow({
webPreferences: {
// ✅ 必须:禁用 Node.js 集成
nodeIntegration: false,
// ✅ 必须:启用上下文隔离
contextIsolation: true,
// ✅ 必须:启用沙箱
sandbox: true,
// ✅ 推荐:预加载脚本
preload: path.join(__dirname, 'preload.js'),
// ✅ 推荐:禁用远程模块(已在 Electron 14+ 移除,但仍需注意)
},
// ✅ 禁用 webSecurity 会绕过同源策略,切勿禁用
webPreferences: {
// webSecurity: false, // ❌ 绝对不要设为 false
},
});session.defaultSession.webRequest 设置 CSP
CSP(Content Security Policy) 是浏览器安全策略的第一道防线:
// main.ts
import { session } from 'electron';
app.whenReady().then(() => {
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
// 生产环境:严格限制
[
[
"default-src 'self'",
// 只允许同源和指定域名
"script-src 'self' https://cdn.example.com",
"style-src 'self' 'unsafe-inline' https://cdn.example.com",
// 限制图片和字体
"img-src 'self' data: https://img.example.com",
"font-src 'self' https://fonts.gstatic.com",
// 禁止 iframe
"frame-src 'none'",
// 禁止 object(防插件注入)
"object-src 'none'",
// 限制 connect 目标
"connect-src 'self' https://api.example.com",
].join('; '),
],
],
},
});
});
});openExternal 白名单
shell.openExternal 是最容易被利用的攻击面——恶意网页可以诱导用户打开任意 URL:
// ❌ 不安全的做法:直接暴露 openExternal
ipcMain.handle('shell:open', (_event, url: string) => {
return shell.openExternal(url); // 任意 URL 都可打开
});
// ✅ 安全的做法:白名单校验
const ALLOWED_PROTOCOLS = ['https:', 'http:'];
const ALLOWED_HOSTS = ['docs.example.com', 'help.example.com', 'github.com'];
ipcMain.handle('shell:open-external', async (_event, url: string) => {
try {
const parsed = new URL(url);
if (!ALLOWED_PROTOCOLS.includes(parsed.protocol)) {
throw new Error(`协议不被允许: ${parsed.protocol}`);
}
// 域名白名单
const isAllowedHost = ALLOWED_HOSTS.some((host) =>
parsed.hostname === host || parsed.hostname.endsWith(`.${host}`)
);
if (!isAllowedHost) {
throw new Error(`域名不在白名单中: ${parsed.hostname}`);
}
await shell.openExternal(url);
return { success: true };
} catch (error) {
return { success: false, error: (error as Error).message };
}
});协议校验
// 防止 file:// 或 chrome:// 等危险协议
ipcMain.handle('shell:open-safe', async (_event, url: string) => {
try {
const parsed = new URL(url);
// 只允许 http/https
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error(`危险协议: ${parsed.protocol}`);
}
// 防止 javascript: 伪协议
if (parsed.protocol === 'javascript:') {
throw new Error('禁止执行 JavaScript 协议');
}
await shell.openExternal(url);
} catch (error) {
console.error('打开链接失败:', error);
}
});禁用危险功能
// main.ts — 禁用危险功能
app.whenReady().then(() => {
// 禁止加载远程内容(需要明确启用)
session.defaultSession.webRequest.onBeforeRequest((details, callback) => {
const url = new URL(details.url);
// 白名单机制:只允许加载指定域名的远程内容
const allowedOrigins = ['self', 'https://cdn.example.com', 'https://fonts.googleapis.com'];
if (details.url.startsWith('http') && !allowedOrigins.some((o) =>
o === 'self' ? url.origin === 'file://' : url.origin === o
)) {
// 非白名单的 http/https 请求需要审计
console.warn('可疑的远程请求:', details.url);
}
callback({ cancel: false });
});
// 禁用 WebBluetooth、WebUSB 等危险 API
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
const allowed = ['notifications', 'fullscreen', 'media'];
if (allowed.includes(permission)) {
callback(true);
} else {
console.warn('拒绝权限请求:', permission);
callback(false);
}
});
});敏感数据加密存储
Electron 提供了 safeStorage API,用于安全地加密敏感数据:
// main.ts — safeStorage 加密敏感信息
import { safeStorage, app } from 'electron';
import fs from 'fs';
import path from 'path';
// 判断 safeStorage 是否可用(某些环境下可能不可用)
if (safeStorage.isEncryptionAvailable()) {
const data = '用户密码或 API Key';
const encrypted = safeStorage.encryptString(data);
// 保存加密后的数据
const filePath = path.join(app.getPath('userData'), 'credentials.enc');
fs.writeFileSync(filePath, encrypted);
// 读取时解密
const encryptedData = fs.readFileSync(filePath);
const decrypted = safeStorage.decryptString(encryptedData);
console.log('解密结果:', decrypted);
} else {
console.warn('safeStorage 不可用,使用备选方案');
// 备选:使用 crypto.createCipher(需要自己管理密钥)
}// 安全的凭证存储封装
import { safeStorage, app } from 'electron';
import fs from 'fs';
import path from 'path';
interface Credentials {
apiKey: string;
token: string;
}
class SecureStore {
private filePath: string;
constructor(filename = 'secure.dat') {
this.filePath = path.join(app.getPath('userData'), filename);
}
save(credentials: Credentials): boolean {
if (!safeStorage.isEncryptionAvailable()) {
console.error('安全存储不可用');
return false;
}
const data = JSON.stringify(credentials);
const encrypted = safeStorage.encryptString(data);
fs.writeFileSync(this.filePath, encrypted);
return true;
}
load(): Credentials | null {
if (!safeStorage.isEncryptionAvailable() || !fs.existsSync(this.filePath)) {
return null;
}
const encrypted = fs.readFileSync(this.filePath);
const decrypted = safeStorage.decryptString(encrypted);
return JSON.parse(decrypted);
}
clear(): void {
if (fs.existsSync(this.filePath)) {
fs.unlinkSync(this.filePath);
}
}
}Electron Fuses 安全加固
Electron Fuses 是在打包阶段对 Electron 二进制进行底层安全加固的工具,通过修改 Electron 可执行文件的熔丝位(fuses)来禁用危险功能:
pnpm add -D @electron/fuses// scripts/set-fuses.js
const { FuseVersion, FuseV1Options, flipFuses } = require('@electron/fuses');
const path = require('path');
async function patchElectronBinary(electronBinaryPath) {
await flipFuses(electronBinaryPath, {
version: FuseVersion.V1,
// 禁用 ELECTRON_RUN_AS_NODE
// 启用后,即使命令行传入 ELECTRON_RUN_AS_NODE=1,Electron 也不会以 Node.js 模式运行
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
});
}
// 使用 electron-builder 的 afterPack 钩子自动应用
module.exports = {
packagerConfig: {
afterPack: async ({ appOutDir }) => {
const electronBinaryPath = path.join(
appOutDir,
process.platform === 'darwin'
? 'MyApp.app/Contents/MacOS/MyApp'
: process.platform === 'win32'
? 'MyApp.exe'
: 'my-app'
);
await patchElectronBinary(electronBinaryPath);
},
},
};Fuses 关键选项说明:
| Fuse | 作用 |
|---|---|
EnableCookieEncryption | 加密 Cookie,防止内存 dump |
EnableNodeOptionsEnvironmentVariable | 禁用 NODE_OPTIONS 环境变量,防止远程代码注入 |
EnableNodeCliInspectArguments | 禁用 --inspect 等调试参数,防止外部调试器接入 |
OnlyLoadAppFromAsar | 只从 asar 包加载应用代码,禁止覆盖本地文件执行 |
EnableEmbeddedAsarIntegrityValidation | 验证 asar 包完整性,防止篡改 |
防止 XSS 和注入攻击
渲染进程中的 XSS
// ❌ 危险:直接插入 HTML
document.getElementById('output').innerHTML = userInput;
// ✅ 安全:使用 textContent
document.getElementById('output').textContent = userInput;
// ✅ 安全:使用 DOMPurify 清理 HTML
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(dirtyHtml, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: ['class'],
});IPC 注入
// ❌ IPC 参数直接用于文件系统操作
ipcMain.handle('file:read', async (_event, filePath: string) => {
// 攻击者可以传递 ../../etc/passwd 等路径遍历
return fs.readFileSync(path.join('/data', filePath));
});
// ✅ 路径白名单验证
ipcMain.handle('file:read', async (_event, filePath: string) => {
const baseDir = path.join(app.getPath('userData'), 'user-files');
const resolved = path.resolve(baseDir, filePath);
if (!resolved.startsWith(baseDir)) {
return { success: false, error: '非法路径' };
}
if (!fs.existsSync(resolved)) {
return { success: false, error: '文件不存在' };
}
return fs.promises.readFile(resolved, 'utf-8');
});WebView 安全
如果应用使用了 <webview> 标签,必须显式启用并限制权限:
<!-- ❌ 使用 webview 前必须显式启用 -->
<webview src="https://example.com" allowpopups></webview>// main.ts — webview 安全配置
webPreferences: {
webviewTag: false, // 禁用 webview(推荐),使用 BrowserView 替代
}
// 如果必须使用 webview,严格限制权限
const webview = document.querySelector('webview');
webview.setSpellcheckEnabled(false);
webview.setAudioMuted(true);安全审计工具
electron-devtools-installer
安装安全相关的 Chrome 扩展:
pnpm add -D electron-devtools-installernpm audit 和 Snyk
# 检查依赖中的已知漏洞
pnpm audit
# 使用 Snyk 进行深度安全扫描
npx snyk test定期检查 Electron 版本
# 检查当前 Electron 版本
npx electron --version
# 查看是否有安全更新
npx npm-check-updates -t electron -uElectron 团队每月都会发布安全补丁,及时升级可以修复已知漏洞。
常见问题
CSP 导致第三方资源加载失败
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com在 CSP 中明确列出需要加载的第三方域名,避免 'unsafe-inline' 滥用。
safeStorage 在 Linux 上不可用
在某些 Linux 发行版上,safeStorage 可能因为缺少加密库而不可用:
if (!safeStorage.isEncryptionAvailable()) {
// 使用基于密码的加密(PBKDF2 + AES)作为备选
console.warn('使用降级加密方案');
}小结
- 基础配置:
nodeIntegration: false+contextIsolation: true+sandbox: true是所有生产应用的底线 - openExternal 必须白名单化,防止任意 URL 打开攻击
- CSP 通过
session.defaultSession.webRequest.onHeadersReceived设置,严格限制资源加载来源 - safeStorage 是 Electron 原生的敏感数据加密方案
- Electron Fuses 在二进制层面禁用危险功能,提供最后一道防线
- IPC 参数校验防止路径遍历和注入攻击
下一篇文章我们将介绍 Electron 性能优化与生产调优,涵盖启动优化、内存管理、GPU 加速控制和打包体积优化等内容。
评论
Written by
AI-Writer
Related Articles
IPC 通信机制详解
深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则
Read MoreElectron 性能优化与生产调优
掌握 Electron 应用的性能优化:启动时间优化、内存泄漏排查、GPU 加速控制、打包体积压缩、多进程架构以及功耗优化
Read MorePreload 脚本与上下文隔离
理解 Preload 脚本的执行时机、contextBridge 安全暴露 API、nodeIntegration 与 contextIsolation 配置组合,以及主进程与渲染进程的安全边界
Read More