electron

Electron 安全最佳实践

By AI-Writer 13 min read

前言

Electron 应用的安全性是一个常被忽视但至关重要的话题。由于 Electron 本质上是一个浏览器内核,Web 安全的所有威胁(XSS、CSRF、注入攻击等)在 Electron 应用中都存在。此外,Electron 的进程架构和 Node.js 集成还引入了额外的攻击面。本文将全面讲解 Electron 应用的安全最佳实践,帮助你构建不易被攻击的桌面应用。

基础安全配置

核心配置组合

这是 Electron 应用最基本的安全配置,也是任何生产应用必须满足的底线:

typescript
// 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) 是浏览器安全策略的第一道防线:

typescript
// 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:

typescript
// ❌ 不安全的做法:直接暴露 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 };
  }
});

协议校验

typescript
// 防止 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);
  }
});

禁用危险功能

typescript
// 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,用于安全地加密敏感数据:

typescript
// 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(需要自己管理密钥)
}
typescript
// 安全的凭证存储封装
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)来禁用危险功能:

bash
pnpm add -D @electron/fuses
javascript
// 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

typescript
// ❌ 危险:直接插入 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 注入

typescript
// ❌ 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> 标签,必须显式启用并限制权限:

html
<!-- ❌ 使用 webview 前必须显式启用 -->
<webview src="https://example.com" allowpopups></webview>
typescript
// main.ts — webview 安全配置
webPreferences: {
  webviewTag: false, // 禁用 webview(推荐),使用 BrowserView 替代
}

// 如果必须使用 webview,严格限制权限
const webview = document.querySelector('webview');
webview.setSpellcheckEnabled(false);
webview.setAudioMuted(true);

安全审计工具

electron-devtools-installer

安装安全相关的 Chrome 扩展:

bash
pnpm add -D electron-devtools-installer

npm audit 和 Snyk

bash
# 检查依赖中的已知漏洞
pnpm audit

# 使用 Snyk 进行深度安全扫描
npx snyk test

定期检查 Electron 版本

bash
# 检查当前 Electron 版本
npx electron --version

# 查看是否有安全更新
npx npm-check-updates -t electron -u

Electron 团队每月都会发布安全补丁,及时升级可以修复已知漏洞。

常见问题

CSP 导致第三方资源加载失败

http
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com

在 CSP 中明确列出需要加载的第三方域名,避免 'unsafe-inline' 滥用。

safeStorage 在 Linux 上不可用

在某些 Linux 发行版上,safeStorage 可能因为缺少加密库而不可用:

typescript
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 加速控制和打包体积优化等内容。

#electron #安全 #CSP #Fuses #safeStorage #XSS

评论

A

Written by

AI-Writer

Related Articles

electron
#3

IPC 通信机制详解

深入理解 Electron 的 IPC 模块:invoke/handle 双向异步模式、ipcRenderer.on 事件监听、channel 命名规范、进程间数据传输规则

Read More
electron
#4

Preload 脚本与上下文隔离

理解 Preload 脚本的执行时机、contextBridge 安全暴露 API、nodeIntegration 与 contextIsolation 配置组合,以及主进程与渲染进程的安全边界

Read More