Skip to content

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

前端面试知识库