Electron 安全实践
上下文隔离、权限控制与安全配置
目录
安全模型
攻击面
┌─────────────────────────────────────────────────────────────┐
│ 潜在攻击途径 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. XSS 攻击 → 如果能访问 Node.js → 系统完全控制 │
│ │
│ 2. 恶意远程内容 → nodeIntegration: true → 危险 │
│ │
│ 3. 不安全的 IPC → 暴露敏感 API → 权限提升 │
│ │
│ 4. 依赖漏洞 → npm 包含恶意代码 → 数据泄露 │
│ │
└─────────────────────────────────────────────────────────────┘安全配置清单
typescript
const win = new BrowserWindow({
webPreferences: {
// ✅ 必须开启
contextIsolation: true, // 隔离预加载脚本和网页
sandbox: true, // 沙箱模式
// ✅ 必须关闭
nodeIntegration: false, // 禁用 Node.js
nodeIntegrationInWorker: false,
nodeIntegrationInSubFrames: false,
enableRemoteModule: false, // 禁用 remote 模块
// ✅ 推荐配置
webSecurity: true, // 同源策略
allowRunningInsecureContent: false
}
});上下文隔离
工作原理
┌─────────────────────────────────────────────────────────────┐
│ contextIsolation: true │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Preload │ │ Renderer │ │
│ │ Context │ ≠ │ Context │ │
│ │ │ │ │ │
│ │ • require() │ │ • window │ │
│ │ • process │ │ • DOM │ │
│ │ • __dirname │ │ • fetch │ │
│ └─────────────────┘ └─────────────────┘ │
│ │ ▲ │
│ │ contextBridge │ │
│ └─────────────────────────────┘ │
│ (唯一安全通道) │
│ │
└─────────────────────────────────────────────────────────────┘安全的 API 暴露
typescript
// preload.js
const { contextBridge, ipcRenderer } = require('electron');
// ✅ 安全:只暴露特定功能
contextBridge.exposeInMainWorld('api', {
// 只读信息
platform: process.platform,
// 受限的 IPC 调用
loadFile: (path) => {
// 验证路径格式
if (!isValidPath(path)) throw new Error('Invalid path');
return ipcRenderer.invoke('file:load', path);
},
// 事件监听(返回清理函数)
onNotification: (callback) => {
const handler = (_, data) => callback(data);
ipcRenderer.on('notification', handler);
return () => ipcRenderer.removeListener('notification', handler);
}
});
// ❌ 危险:暴露整个 ipcRenderer
contextBridge.exposeInMainWorld('electron', { ipcRenderer });主进程验证
typescript
// main.js
const allowedChannels = ['file:load', 'file:save', 'app:version'];
ipcMain.handle('file:load', async (event, filePath) => {
// 1. 验证来源
if (!validateSender(event.senderFrame)) {
throw new Error('Invalid sender');
}
// 2. 验证路径
const safeDir = app.getPath('userData');
const resolved = path.resolve(filePath);
if (!resolved.startsWith(safeDir)) {
throw new Error('Access denied');
}
// 3. 执行操作
return fs.readFileSync(resolved, 'utf-8');
});
function validateSender(frame) {
const url = new URL(frame.url);
return url.protocol === 'file:' || url.host === 'localhost';
}权限控制
权限请求处理
typescript
const { session } = require('electron');
// 处理权限请求
session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
const url = webContents.getURL();
// 允许的权限列表
const allowedPermissions = {
'notifications': ['file://', 'https://myapp.com'],
'media': ['file://'],
'geolocation': [] // 全部禁止
};
const allowed = allowedPermissions[permission]?.some(
pattern => url.startsWith(pattern)
);
console.log(`Permission ${permission} for ${url}: ${allowed}`);
callback(allowed);
});
// 处理权限检查
session.defaultSession.setPermissionCheckHandler((webContents, permission) => {
// 返回 true/false 表示是否已授权
return false;
});沙箱模式
typescript
// 启用沙箱
new BrowserWindow({
webPreferences: {
sandbox: true, // 渲染进程在沙箱中运行
preload: path.join(__dirname, 'preload.js')
}
});
// 沙箱限制:
// - 无法直接访问 Node.js API
// - 文件系统访问受限
// - 进程创建受限远程内容安全
加载远程内容的风险
typescript
// ❌ 危险配置
new BrowserWindow({
webPreferences: {
nodeIntegration: true // 远程内容可执行任意代码
}
}).loadURL('https://example.com');
// ✅ 安全配置
new BrowserWindow({
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
sandbox: true
}
}).loadURL('https://example.com');Content Security Policy
typescript
// 设置 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.myapp.com"
].join('; ')
}
});
});导航控制
typescript
// 限制导航
win.webContents.on('will-navigate', (event, url) => {
const allowed = ['https://myapp.com', 'file://'];
if (!allowed.some(prefix => url.startsWith(prefix))) {
event.preventDefault();
shell.openExternal(url); // 在系统浏览器打开
}
});
// 阻止新窗口
win.webContents.setWindowOpenHandler(({ url }) => {
shell.openExternal(url);
return { action: 'deny' };
});安全清单
必须配置
typescript
const secureDefaults = {
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
sandbox: true,
webSecurity: true,
allowRunningInsecureContent: false,
enableRemoteModule: false
}
};代码审查要点
- [ ] 所有 IPC channel 都有输入验证
- [ ] 文件路径操作都有边界检查
- [ ] 不直接暴露 ipcRenderer
- [ ] 不使用
eval()或new Function() - [ ] 使用 CSP 限制脚本来源
- [ ] 验证导航目标 URL
高频面试题
Q1: 为什么 contextIsolation 是必须的?
防止原型链污染:
javascript
// 关闭 contextIsolation 时,恶意网页可以:
Array.prototype.forEach = function() {
// 劫持 preload 脚本中的数组操作
// 获取敏感数据
};Q2: nodeIntegration: true 的风险?
RCE(远程代码执行):
html
<!-- 如果加载了恶意页面 -->
<script>
require('child_process').exec('rm -rf /');
</script>Q3: 如何安全地使用 shell.openExternal?
typescript
// ✅ 验证 URL 协议
function safeOpenExternal(url) {
const parsed = new URL(url);
if (!['http:', 'https:', 'mailto:'].includes(parsed.protocol)) {
throw new Error('Invalid protocol');
}
shell.openExternal(url);
}
// ❌ 直接打开用户输入
shell.openExternal(userInput); // 可能执行 file:// 或自定义协议Q4: 沙箱模式有什么限制?
| 能力 | 沙箱开启 | 沙箱关闭 |
|---|---|---|
| Node.js 原生模块 | ❌ | ✅ |
| 子进程 | ❌ | ✅ |
| 文件系统 | 受限 | ✅ |
| IPC | ✅ | ✅ |