Electron 安全模型
Electron 的安全模型建立在隔离和最小权限原则之上:
- 上下文隔离:渲染进程和 Node.js 环境隔离
- 节点集成禁用:渲染进程默认不能访问 Node.js
- 沙箱:操作系统级别的进程隔离
- 内容安全策略:控制资源加载和脚本执行
安全架构图
┌─────────────────────────────────────────────┐
│ Main Process │
│ ┌─────────────────────────────────────┐ │
│ │ Node.js Environment │ │
│ │ - File System │ │
│ │ - Native APIs │ │
│ │ - Child Process │ │
│ └─────────────────────────────────────┘ │
│ │ IPC (contextBridge) │
│ │ (filtered API) │
├──────────────────────┼──────────────────────┤
│ Preload Script │
│ ┌─────────────────────────────────────┐ │
│ │ - Exposed APIs (via contextBridge) │ │
│ │ - Validated inputs │ │
│ └─────────────────────────────────────┘ │
│ │ │
├──────────────────────┼──────────────────────┤
│ Renderer Process │
│ ┌─────────────────────────────────────┐ │
│ │ Chromium Environment │ │
│ │ - DOM, CSS │ │
│ │ - Web APIs │ │
│ │ - NO Node.js access │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘
关键配置
必开的安全配置
const mainWindow = new BrowserWindow({
webPreferences: {
// 1. 启用上下文隔离(MUST HAVE)
contextIsolation: true,
// 2. 禁用 Node.js 集成(MUST HAVE)
nodeIntegration: false,
// 3. 启用沙箱(MUST HAVE)
sandbox: true,
// 4. 指定 preload 脚本(MUST HAVE)
preload: path.join(__dirname, 'preload.js'),
// 5. 禁用 webSecurity(NEVER disable in production)
webSecurity: true,
// 6. 禁用远程模块(MUST HAVE)
enableRemoteModule: false,
// 7. 启用建议策略(MUST HAVE)
webviewTag: false // 如果不使用 webview
}
});
配置检查清单
| 配置项 | 推荐值 | 原因 |
|---|---|---|
| contextIsolation | true |
防止 prototype 污染 |
| nodeIntegration | false |
最小权限原则 |
| sandbox | true |
操作系统级隔离 |
| webSecurity | true |
防止跨域攻击 |
| enableRemoteModule | false |
防止远程代码执行 |
内容安全策略(CSP)
配置 CSP
// main.js
mainWindow.webContents.session.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; " +
"frame-src 'none';"
]
}
});
});
CSP 指令说明
- default-src:默认来源
- script-src:JavaScript 来源
- style-src:CSS 来源
- img-src:图片来源
- connect-src:XHR/Fetch/WebSocket 目标
- frame-src:iframe 来源
HTML 中内联 CSP
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
contextBridge 安全使用
安全的 preload 实现
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// 验证输入的辅助函数
function validatePath(filePath) {
const path = require('path');
const { app } = require('electron');
// 确保路径在允许的目录内
const allowedDir = app.getPath('userData');
const resolved = path.resolve(filePath);
if (!resolved.startsWith(allowedDir)) {
throw new Error('Access denied: path outside allowed directory');
}
return resolved;
}
// 暴露安全的 API
contextBridge.exposeInMainWorld('electronAPI', {
// 文件操作 - 带验证
readFile: async (filePath) => {
const safePath = validatePath(filePath);
return await ipcRenderer.invoke('file:read', safePath);
},
writeFile: async (filePath, content) => {
const safePath = validatePath(filePath);
return await ipcRenderer.invoke('file:write', safePath, content);
},
// 限制性 API - 不暴露原始功能
openExternal: (url) => {
// 只允许 http/https
try {
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Only http/https URLs allowed');
}
return ipcRenderer.invoke('shell:openExternal', url);
} catch (e) {
throw new Error('Invalid URL');
}
},
// 移除危险功能
// NOT exposing: child_process, require, process.exit, etc.
});
危险的反面示例
// 危险:暴露整个 ipcRenderer
contextBridge.exposeInMainWorld('api', ipcRenderer);
// 危险:暴露 Node.js
contextBridge.exposeInMainWorld('node', { require, process });
// 危险:无限制的文件访问
contextBridge.exposeInMainWorld('api', {
readFile: (path) => require('fs').readFileSync(path, 'utf8')
});
主进程安全处理
IPC 输入验证
// 危险:直接使用用户输入
ipcMain.handle('file:read', async (event, filePath) => {
return fs.readFileSync(filePath); // 路径穿越漏洞
});
// 安全:验证路径
ipcMain.handle('file:read', async (event, filePath) => {
// 验证文件路径
const { app } = require('electron');
const path = require('path');
const allowedDir = app.getPath('userData');
const resolvedPath = path.resolve(filePath);
// 防止路径穿越
if (!resolvedPath.startsWith(allowedDir + path.sep)) {
throw new Error('Access denied');
}
// 检查文件是否存在
try {
await fs.promises.access(resolvedPath, fs.constants.R_OK);
} catch {
throw new Error('File not accessible');
}
return await fs.promises.readFile(resolvedPath, 'utf8');
});
Shell 命令执行
// 危险:用户输入作为命令的一部分
ipcMain.handle('exec', async (event, cmd) => {
const { exec } = require('child_process');
return new Promise((resolve, reject) => {
exec(cmd, (error, stdout, stderr) => { // 命令注入漏洞
if (error) reject(error);
else resolve(stdout);
});
});
});
// 安全:使用参数化
ipcMain.handle('list-dir', async (event, dirName) => {
const { exec } = require('child_process');
// 白名单验证目录名
if (!/^[a-zA-Z0-9_-]+$/.test(dirName)) {
throw new Error('Invalid directory name');
}
return new Promise((resolve, reject) => {
exec(`ls ${dirName}`, (error, stdout, stderr) => {
if (error) reject(error);
else resolve(stdout);
});
});
});
远程内容处理
处理外部链接
// main.js
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// 只允许 http/https
if (url.startsWith('http://') || url.startsWith('https://')) {
require('electron').shell.openExternal(url);
}
return { action: 'deny' }; // 阻止默认行为
});
阻止新窗口创建
mainWindow.webContents.on('new-window', (event, url) => {
event.preventDefault();
// 改为在当前窗口导航
mainWindow.loadURL(url);
});
安全加载外部 URL
// 如果需要加载外部 URL
async function loadExternalURL(url) {
// 验证 URL
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Invalid protocol');
}
// 在隔离的 BrowserWindow 中加载
const childWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
preload: path.join(__dirname, 'preload-external.js')
}
});
await childWindow.loadURL(url);
}
会话安全
安全 cookie 设置
const { session } = require('electron');
// 配置会话 cookie
session.defaultSession.cookies.get({ name: 'session' }, (error, cookies) => {
console.log(cookies);
});
// 设置安全 cookie
session.defaultSession.cookies.set({
url: 'https://example.com',
name: 'session',
value: 'secure-session-token',
httpOnly: true, // 禁止 JavaScript 访问
secure: true, // 只通过 HTTPS 传输
sameSite: 'strict' // 防止 CSRF
});
清除敏感数据
// 退出时清除所有数据
app.on('before-quit', () => {
session.defaultSession.clearStorageData();
session.defaultSession.clearCache();
});
常见攻击向量与防护
1. 路径穿越
攻击者通过 ../../../etc/passwd 访问系统文件。
防护:使用 path.resolve 并验证路径前缀。
2. 命令注入
攻击者通过 ; rm -rf / 执行恶意命令。
防护:使用参数化、白名单验证、避免 shell 执行。
3. XSS(跨站脚本)
在 webContent 中执行恶意脚本。
防护:启用 CSP、禁用 nodeIntegration、验证所有输入。
4. 点击劫持
将隐藏的 iframe 覆盖在按钮上,诱导用户点击。
防护:
mainWindow.setAlwaysOnTop(true);
// 或者在 HTTP 头中设置 X-Frame-Options
5. MITM(中间人攻击)
攻击者拦截和篡改网络通信。
防护:
// 验证证书
session.setCertificateVerifyProc((host, certificate, valid) => {
// 实现证书验证逻辑
});
安全审计清单
发布前检查
-
nodeIntegration设为false -
contextIsolation设为true -
sandbox设为true - 禁用
enableRemoteModule - 禁用
webviewTag(如不使用) - CSP 头已配置
- 所有 IPC 输入已验证
- 禁止
file://协议加载外部内容 - 敏感数据使用
safeStorage加密 - Session cookie 设置
httpOnly和secure
依赖安全
# 定期检查漏洞
npm audit
npx security-checker
# 使用 Snyk
npx snyk test
安全工具
electron-builder 配置签名
{
"build": {
"mac": {
"hardenedRuntime": true,
"entitlements": "entitlements.plist"
}
}
}
安全扫描
# Electron 安全扫描
npx @electron/security-checker
# 依赖检查
npx npm-check-updates
这一章想说的
Electron 安全核心原则:
- 最小权限:禁用不必要的功能,只暴露必需的 API
- 上下文隔离:渲染进程和 Node.js 完全隔离
- 输入验证:所有来自渲染进程的输入都必须验证
- CSP:配置严格的内容安全策略
- 安全配置:使用推荐的安全配置选项
安全不是事后补救,而是设计时就要考虑的核心原则。