安全最佳实践

深入理解 Electron 应用的安全问题、常见攻击向量、以及如何构建安全的 Electron 应用,包括 CSP、内容安全策略、上下文隔离等核心概念。


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 设置 httpOnlysecure

依赖安全

# 定期检查漏洞
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 安全核心原则:

  1. 最小权限:禁用不必要的功能,只暴露必需的 API
  2. 上下文隔离:渲染进程和 Node.js 完全隔离
  3. 输入验证:所有来自渲染进程的输入都必须验证
  4. CSP:配置严格的内容安全策略
  5. 安全配置:使用推荐的安全配置选项

安全不是事后补救,而是设计时就要考虑的核心原则。


延展阅读