{T}

Electron 安全模型

Electron 的安全模型基于 Chromium 的多进程架构和 Node.js 的集成。理解安全模型对于构建安全的 Electron 应用至关重要。本文档详细介绍 Electron 的安全架构、核心安全配置及最佳实践。

架构概述

Electron 采用多进程架构,继承自 Chromium 的设计理念。每个进程有明确的职责边界和安全限制。

code
┌─────────────────────────────────────────────────────────────┐
│                      Main Process                            │
│  主进程 - 单一实例                                            │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ • 完全访问 Node.js 和 Electron API                   │    │
│  │ • 管理窗口(BrowserWindow)和应用生命周期             │    │
│  │ • 处理系统级操作(文件、原生菜单、对话框等)          │    │
│  │ • 作为唯一可信源处理敏感操作                          │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
           │ IPC (进程间通信 - 异步消息传递)
           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Preload Script                            │
│  预加载脚本 - 每个渲染进程一个                                │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ • 在渲染进程创建前执行                                │    │
│  │ • 访问有限的 Node.js API 子集                        │    │
│  │ • 通过 contextBridge 安全暴露 API 给渲染进程          │    │
│  │ • 作为主进程与渲染进程之间的桥梁                      │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
           │ contextBridge.exposeInMainWorld()
           ▼
┌─────────────────────────────────────────────────────────────┐
│                   Renderer Process                           │
│  渲染进程 - 可多个实例                                        │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ • 运行 Web 内容(HTML/CSS/JavaScript)               │    │
│  │ • 默认无 Node.js 访问权限                            │    │
│  │ • 通过暴露的 API 与主进程通信                        │    │
│  │ • 可能加载不可信内容,需要严格隔离                    │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

安全数据流向

code
┌──────────────┐     IPC 消息验证      ┌──────────────┐
│   Renderer   │ ──────────────────► │     Main     │
│  (不可信区)   │     严格校验参数      │   (可信区)    │
│              │ ◄────────────────── │              │
└──────────────┘     返回安全数据      └──────────────┘
        │                                     │
        │ contextBridge                       │
        ▼                                     ▼
┌──────────────┐                    ┌──────────────┐
│  Preload.js  │                    │  Node.js API │
│  (受限访问)   │                    │  (完全访问)  │
└──────────────┘                    └──────────────┘

进程职责与安全边界

主进程(Main Process)

职责说明安全注意
应用生命周期管理 app 模块的生命周期事件避免在 will-quit 前泄露敏感数据
窗口管理创建和管理 BrowserWindow 实例严格配置 webPreferences
IPC 处理处理来自渲染进程的请求验证所有输入参数
原生集成菜单、托盘、快捷键等权限最小化原则
文件系统读写文件、访问数据库路径验证、权限控制

预加载脚本(Preload Script)

职责说明安全注意
API 暴露通过 contextBridge 暴露安全 API使用白名单机制
数据转换处理渲染进程与主进程间的数据传递过滤敏感信息
权限控制限制渲染进程可执行的操作最小暴露原则

渲染进程(Renderer Process)

职责说明安全注意
UI 渲染渲染用户界面防止 XSS 攻击
用户交互处理用户输入输入验证和清理
网络请求发送 HTTP 请求(如有权限)使用 HTTPS

核心安全配置

配置参数总览

参数推荐值默认值说明
contextIsolationtruetrue (v12+)上下文隔离
nodeIntegrationfalsefalseNode.js 集成
sandboxtruetrue (v20+)沙箱模式
webSecuritytruetrueWeb 安全策略
allowRunningInsecureContentfalsefalse允许混合内容
enableRemoteModulefalsefalse (v14+)远程模块
nodeIntegrationInWorkerfalsefalseWorker 中 Node 集成
nodeIntegrationInSubFramesfalsefalse子框架中 Node 集成

1. contextIsolation(上下文隔离)

状态:从 Electron v12 开始默认启用

必须启用,这是最重要的安全设置:

javascript
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true  // 默认为 true,强烈建议保持
  }
})

工作原理

code
┌─────────────────────────────────────────────────────────────┐
│                    Preload Context                           │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ const secret = 'SENSITIVE_DATA'                     │    │
│  │ const ipcRenderer = require('electron').ipcRenderer │    │
│  └─────────────────────────────────────────────────────┘    │
│                          │                                   │
│                          │ contextBridge (只读暴露)          │
│                          ▼                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ window.safeAPI = {                                  │    │
│  │   sendMessage: (msg) => ipcRenderer.send(msg)       │    │
│  │ }                                                   │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘
           │ 隔离边界(无法穿越)
           ▼
┌─────────────────────────────────────────────────────────────┐
│                    Renderer Context                          │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ // 无法访问 secret 或 ipcRenderer                    │    │
│  │ window.safeAPI.sendMessage('hello') // ✓ 可用       │    │
│  │ console.log(secret) // ✗ undefined                  │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

防护效果

  • ✅ 隔离 preload 脚本和渲染进程的 JavaScript 上下文
  • ✅ 防止渲染进程直接访问 preload 中的变量
  • ✅ 防止原型污染攻击
  • ✅ 阻止渲染进程修改预加载脚本的全局对象

2. nodeIntegration(Node.js 集成)

状态:默认禁用

必须禁用

javascript
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: false  // 默认为 false,必须保持
  }
})

危险示例

javascript
// ❌ 极度危险配置
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,      // 允许渲染进程访问 Node.js
    contextIsolation: false,    // 无上下文隔离
    webSecurity: false          // 禁用同源策略
  }
})
win.loadURL('https://untrusted-site.com')
// 后果:远程网站可执行任意系统命令
// <script>require('child_process').exec('rm -rf /')</script>

禁用原因

风险说明
远程代码执行加载远程内容时可执行任意 Node.js 代码
文件系统访问可读写任意本地文件
系统命令执行可执行 shell 命令
进程管理可创建子进程

3. sandbox(沙箱模式)

状态:从 Electron v20 开始默认启用

推荐启用

javascript
const win = new BrowserWindow({
  webPreferences: {
    sandbox: true  // 启用沙箱
  }
})

沙箱限制

code
┌─────────────────────────────────────────────────────────────┐
│                    Sandbox 环境                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ ✗ 无文件系统访问 (fs)                                │    │
│  │ ✗ 无子进程创建 (child_process)                      │    │
│  │ ✗ 无原生模块加载                                     │    │
│  │ ✗ 无系统信息获取 (os)                                │    │
│  │ ✓ 仅可通过 IPC 与主进程通信                          │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Preload 脚本在沙箱中的行为

javascript
// preload.js (sandbox: true 时)
const { contextBridge, ipcRenderer } = require('electron')

// ✅ 可用:ipcRenderer
// ✅ 可用:contextBridge
// ✅ 可用:process 对象(有限)
// ❌ 不可用:fs、child_process、原生模块

contextBridge.exposeInMainWorld('electronAPI', {
  readFile: (path) => ipcRenderer.invoke('file:read', path)
})

4. webSecurity(Web 安全策略)

状态:默认启用

不要禁用

javascript
const win = new BrowserWindow({
  webPreferences: {
    webSecurity: true  // 默认为 true,不要禁用
  }
})

禁用后果

javascript
// ❌ 禁用 webSecurity 后
// 1. 同源策略失效
// 2. 可跨域读取任意网站数据
// 3. 可加载混合内容(HTTP + HTTPS)
// 4. 易受 CSRF 攻击

正确处理跨域

javascript
// ✅ 使用主进程代理跨域请求
// main.js
ipcMain.handle('api:fetch', async (event, url, options) => {
  const response = await fetch(url, options)
  return response.json()
})

// preload.js
contextBridge.exposeInMainWorld('api', {
  fetch: (url, options) => ipcRenderer.invoke('api:fetch', url, options)
})

安全的窗口配置

推荐配置模板

javascript
const { app, BrowserWindow } = require('electron')
const path = require('path')

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      // === 安全配置(必须)===
      contextIsolation: true,              // 上下文隔离
      nodeIntegration: false,              // 禁用 Node.js 集成
      webSecurity: true,                   // 启用 Web 安全
      allowRunningInsecureContent: false,  // 禁止混合内容
      enableRemoteModule: false,           // 禁用远程模块
      
      // === 安全配置(推荐)===
      sandbox: true,                       // 启用沙箱
      nodeIntegrationInWorker: false,      // 禁用 Worker Node 集成
      nodeIntegrationInSubFrames: false,   // 禁用子框架 Node 集成
      
      // === 预加载脚本 ===
      preload: path.join(__dirname, 'preload.js')
    }
  })
  
  win.loadFile('index.html')
}

app.whenReady().then(createWindow)

不同场景的配置

场景一:纯本地应用

javascript
// 加载本地 HTML,无远程内容
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    preload: path.join(__dirname, 'preload.js')
  }
})
win.loadFile('index.html')

场景二:加载可信远程内容

javascript
// 加载可信的远程服务
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    webSecurity: true,
    // 不设置 preload,或仅暴露有限 API
  }
})
win.loadURL('https://trusted-app.example.com')

场景三:开发环境

javascript
// 开发时可能需要额外配置
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    preload: path.join(__dirname, 'preload.js')
  }
})

// 仅开发环境启用 DevTools
if (process.env.NODE_ENV === 'development') {
  win.webContents.openDevTools()
}

安全的 IPC 通信

通信模式

code
┌──────────────┐                          ┌──────────────┐
│   Renderer   │                          │     Main     │
│              │   ipcRenderer.invoke()   │              │
│              │ ──────────────────────► │              │
│              │   ipcMain.handle()       │              │
│              │ ◄────────────────────── │              │
│              │   返回结果               │              │
└──────────────┘                          └──────────────┘

Preload 脚本安全模式

javascript
// preload.js
const { contextBridge, ipcRenderer } = require('electron')

// 定义允许的通道白名单
const ALLOWED_CHANNELS = {
  invoke: ['dialog:open', 'file:read', 'file:write', 'app:getVersion'],
  send: ['window:minimize', 'window:maximize', 'window:close'],
  on: ['update:available', 'update:progress', 'notification:show']
}

// 验证通道是否在白名单中
function validateChannel(type, channel) {
  if (!ALLOWED_CHANNELS[type]?.includes(channel)) {
    console.error(`Blocked invalid IPC channel: ${channel}`)
    return false
  }
  return true
}

// 安全暴露 API
contextBridge.exposeInMainWorld('electronAPI', {
  // 双向通信(返回 Promise)
  invoke: (channel, ...args) => {
    if (!validateChannel('invoke', channel)) {
      return Promise.reject(new Error(`Invalid channel: ${channel}`))
    }
    return ipcRenderer.invoke(channel, ...args)
  },
  
  // 单向发送
  send: (channel, ...args) => {
    if (!validateChannel('send', channel)) {
      return
    }
    ipcRenderer.send(channel, ...args)
  },
  
  // 监听事件
  on: (channel, callback) => {
    if (!validateChannel('on', channel)) {
      return () => {}
    }
    const subscription = (event, ...args) => callback(...args)
    ipcRenderer.on(channel, subscription)
    // 返回取消订阅函数
    return () => ipcRenderer.removeListener(channel, subscription)
  },
  
  // 一次性监听
  once: (channel, callback) => {
    if (!validateChannel('on', channel)) {
      return
    }
    ipcRenderer.once(channel, (event, ...args) => callback(...args))
  }
})

主进程验证

javascript
// main.js
const { app, BrowserWindow, ipcMain, dialog } = require('electron')
const path = require('path')
const fs = require('fs')

// 允许访问的基础目录
const ALLOWED_BASE_PATHS = [
  app.getPath('documents'),
  app.getPath('downloads'),
  app.getPath('userData')
]

// 验证发送者
function isValidSender(webContentsId) {
  // 实现你的验证逻辑,例如检查是否为已知窗口
  return true
}

// 验证路径安全性
function isPathSafe(requestedPath, basePaths = ALLOWED_BASE_PATHS) {
  const normalizedPath = path.normalize(requestedPath)
  return basePaths.some(basePath => 
    normalizedPath.startsWith(path.normalize(basePath))
  )
}

// 文件读取处理器
ipcMain.handle('file:read', async (event, filePath) => {
  // 1. 验证来源
  if (!isValidSender(event.sender.id)) {
    throw new Error('Unauthorized sender')
  }
  
  // 2. 验证参数类型
  if (typeof filePath !== 'string' || filePath.length === 0) {
    throw new TypeError('filePath must be a non-empty string')
  }
  
  // 3. 路径安全检查
  const resolvedPath = path.resolve(filePath)
  if (!isPathSafe(resolvedPath)) {
    throw new Error(`Access denied: path outside allowed directories`)
  }
  
  // 4. 检查文件是否存在
  if (!fs.existsSync(resolvedPath)) {
    throw new Error(`File not found: ${resolvedPath}`)
  }
  
  // 5. 执行操作
  try {
    const content = await fs.promises.readFile(resolvedPath, 'utf-8')
    return { success: true, content }
  } catch (error) {
    throw new Error(`Failed to read file: ${error.message}`)
  }
})

// 文件写入处理器
ipcMain.handle('file:write', async (event, filePath, content) => {
  // 参数验证
  if (typeof filePath !== 'string') {
    throw new TypeError('filePath must be a string')
  }
  if (typeof content !== 'string') {
    throw new TypeError('content must be a string')
  }
  
  // 路径验证
  const resolvedPath = path.resolve(filePath)
  if (!isPathSafe(resolvedPath)) {
    throw new Error('Access denied: path outside allowed directories')
  }
  
  // 写入文件
  await fs.promises.writeFile(resolvedPath, content, 'utf-8')
  return { success: true }
})

// 对话框处理器
ipcMain.handle('dialog:open', async (event, options = {}) => {
  const result = await dialog.showOpenDialog({
    title: options.title || '选择文件',
    defaultPath: options.defaultPath,
    filters: options.filters || [{ name: 'All Files', extensions: ['*'] }],
    properties: ['openFile', ...(options.multiSelections ? ['multiSelections'] : [])]
  })
  
  // 返回选择的文件路径(已经是绝对路径)
  return result.filePaths
})

渲染进程使用

javascript
// renderer.js
async function readFile() {
  try {
    const result = await window.electronAPI.invoke('file:read', '/path/to/file.txt')
    console.log('File content:', result.content)
  } catch (error) {
    console.error('Failed to read file:', error.message)
  }
}

// 监听事件(返回取消订阅函数)
const unsubscribe = window.electronAPI.on('update:available', (info) => {
  console.log('Update available:', info)
})

// 取消监听
// unsubscribe()

内容安全策略(CSP)

什么是 CSP

内容安全策略(Content Security Policy)是一层额外的安全保护,用于限制资源加载来源,防止 XSS 和数据注入攻击。

设置方式

方式一:HTML Meta 标签

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy" content="
    default-src 'self';
    script-src 'self';
    style-src 'self' 'unsafe-inline';
    img-src 'self' data: https:;
    connect-src 'self' https://api.example.com;
    font-src 'self' data:;
  ">
  <title>My Electron App</title>
</head>
<body>
  <!-- 内容 -->
</body>
</html>

方式二:主进程设置

javascript
const { session } = require('electron')

// 为默认会话设置 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.example.com;"
      ]
    }
  })
})

CSP 指令详解

指令说明示例
default-src默认资源来源'self' 仅限同源
script-srcJavaScript 来源'self' 不允许内联脚本
style-srcCSS 来源'self' 'unsafe-inline'
img-src图片来源'self' data: https:
connect-src网络请求目标'self' https://api.example.com
font-src字体来源'self' data:
media-src媒体来源'self'
object-src插件来源'none' 禁止插件

不同场景的 CSP 配置

严格模式(推荐用于本地应用)

html
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self' data:;
  connect-src 'self';
  font-src 'self';
">

开发模式

html
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' 'unsafe-eval' 'unsafe-inline';
  style-src 'self' 'unsafe-inline';
  connect-src 'self' ws://localhost:* http://localhost:*;
">

允许特定 CDN

html
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self';
  script-src 'self' https://cdn.jsdelivr.net;
  style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
  font-src 'self' https://fonts.gstatic.com;
  img-src 'self' data: https:;
">

远程内容安全

安全原则

  1. 避免加载不受信任的远程内容
  2. 如需加载,使用严格的安全配置
  3. 验证所有导航和窗口创建请求

危险示例

javascript
// ❌ 极度危险:加载远程内容且启用 Node.js
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,
    contextIsolation: false
  }
})
win.loadURL('https://untrusted-site.com')
// 攻击者可在网页中执行:require('child_process').exec('malware')

安全示例

javascript
// ✅ 安全:加载本地内容
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    preload: path.join(__dirname, 'preload.js')
  }
})
win.loadFile('index.html')

// ✅ 如必须加载远程内容,使用严格限制
const remoteWin = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    webSecurity: true,
    enableRemoteModule: false,
    // 不设置 preload,或仅暴露非常有限的 API
  }
})
remoteWin.loadURL('https://trusted-site.com')

导航验证

javascript
const { app, shell } = require('electron')

// 全局导航控制
app.on('web-contents-created', (event, contents) => {
  // 阻止导航到非白名单 URL
  contents.on('will-navigate', (event, navigationUrl) => {
    const parsedUrl = new URL(navigationUrl)
    
    // 白名单检查
    const allowedOrigins = [
      'https://myapp.com',
      'https://api.myapp.com'
    ]
    
    if (!allowedOrigins.includes(parsedUrl.origin)) {
      console.warn(`Blocked navigation to: ${navigationUrl}`)
      event.preventDefault()
    }
  })
  
  // 控制新窗口打开
  contents.setWindowOpenHandler(({ url }) => {
    // 在默认浏览器中打开外部链接
    if (url.startsWith('http://') || url.startsWith('https://')) {
      shell.openExternal(url)
    }
    // 阻止 Electron 创建新窗口
    return { action: 'deny' }
  })
  
  // 阻止 iframe 嵌入不可信内容
  contents.on('will-attach-webview', (event, webPreferences, params) => {
    // 移除 preload 路径
    delete webPreferences.preload
    
    // 禁用 Node.js
    webPreferences.nodeIntegration = false
    webPreferences.contextIsolation = true
  })
})

常见攻击向量与防护

1. XSS(跨站脚本攻击)

javascript
// ❌ 危险:直接插入用户内容
document.innerHTML = userInput
element.insertAdjacentHTML('beforeend', userInput)

// ✅ 安全:使用 textContent
element.textContent = userInput

// ✅ 安全:使用 DOM API
const div = document.createElement('div')
div.textContent = userInput
container.appendChild(div)

XSS 防护清单

  • 使用 textContent 而非 innerHTML
  • 对用户输入进行转义
  • 设置严格的 CSP
  • 启用 contextIsolation
  • 禁用 nodeIntegration

2. 原型污染攻击

javascript
// ❌ 危险:可被污染原型
const obj = {}
obj.__proto__.polluted = true  // 污染 Object.prototype

// ✅ 安全:使用 Object.create(null)
const obj = Object.create(null)

// ✅ 安全:使用 Map 代替普通对象
const map = new Map()
map.set('key', 'value')

原型污染防护

javascript
// 在 preload.js 中,contextIsolation 已提供保护
// 但仍需注意以下情况:

// ❌ 危险:直接合并用户输入
Object.assign(target, userInput)

// ✅ 安全:使用 Object.defineProperty
Object.defineProperty(target, key, {
  value: userInput[key],
  writable: true,
  configurable: true,
  enumerable: true
})

// ✅ 安全:使用深度克隆
function safeClone(obj) {
  return JSON.parse(JSON.stringify(obj))
}

3. 路径遍历攻击

javascript
// ❌ 危险:未验证路径
const content = fs.readFileSync(userInput, 'utf-8')
// userInput 可能是 "../../../../etc/passwd"

// ✅ 安全:验证路径
const path = require('path')

function safeReadFile(userInput, baseDir) {
  // 解析为绝对路径
  const resolvedPath = path.resolve(baseDir, userInput)
  
  // 确保路径在允许的目录内
  if (!resolvedPath.startsWith(path.resolve(baseDir))) {
    throw new Error('Access denied: path outside base directory')
  }
  
  return fs.readFileSync(resolvedPath, 'utf-8')
}

4. 代码注入攻击

javascript
// ❌ 危险:执行动态代码
eval(userInput)
new Function(userInput)()
setTimeout(userInput, 0)
setInterval(userInput, 0)

// ✅ 安全:避免执行动态代码
// 使用预定义的函数映射
const actions = {
  'save': saveFile,
  'load': loadFile,
  'delete': deleteFile
}

function executeAction(actionName, ...args) {
  const action = actions[actionName]
  if (typeof action === 'function') {
    return action(...args)
  }
  throw new Error(`Unknown action: ${actionName}`)
}

5. 中间人攻击

javascript
// ❌ 危险:加载 HTTP 内容
win.loadURL('http://example.com')

// ❌ 危险:禁用证书验证
app.commandLine.appendSwitch('ignore-certificate-errors')

// ✅ 安全:仅加载 HTTPS 内容
win.loadURL('https://example.com')

// ✅ 安全:证书验证(生产环境)
app.on('certificate-error', (event, webContents, url, error, certificate, callback) => {
  // 仅信任特定证书
  if (certificate.issuerName === 'My Trusted CA') {
    callback(true)  // 信任
  } else {
    callback(false)  // 拒绝
  }
  event.preventDefault()
})

安全检查清单

必须项(Critical)

检查项说明验证方式
contextIsolation: true上下文隔离检查 webPreferences
nodeIntegration: false禁用 Node.js 集成检查 webPreferences
webSecurity: true启用 Web 安全检查 webPreferences
enableRemoteModule: false禁用远程模块检查 webPreferences
使用 contextBridge安全暴露 API检查 preload 脚本
IPC 通道白名单限制可用的 IPC 通道检查 preload 脚本
设置 CSP内容安全策略检查 HTML 或主进程

推荐项(Recommended)

检查项说明
sandbox: true启用沙箱模式
验证 IPC 参数主进程验证所有输入
限制文件访问路径使用白名单目录
导航验证监听 will-navigate
新窗口控制使用 setWindowOpenHandler

禁止项(Forbidden)

检查项风险
contextIsolation: false原型污染、API 泄露
nodeIntegration: true远程代码执行
webSecurity: false同源策略失效
enableRemoteModule: true安全漏洞
❌ 暴露整个 ipcRenderer权限过大
❌ 加载不信任的远程内容远程代码执行

常见问题解答

Q1: 为什么禁用 nodeIntegration 后,渲染进程无法使用 require

A: 这是安全设计的预期行为。渲染进程不应直接访问 Node.js API。正确做法是通过 preload 脚本和 contextBridge 暴露需要的 API:

javascript
// preload.js
const { contextBridge } = require('electron')
const fs = require('fs')

contextBridge.exposeInMainWorld('nodeAPI', {
  readFile: (path) => fs.readFileSync(path, 'utf-8')
})

Q2: sandbox: truesandbox: false 有什么区别?

A:

特性sandbox: falsesandbox: true
Preload 脚本 Node.js 访问完整受限
原生模块可用不可用
安全性较低更高

推荐启用沙箱,需要在主进程处理文件系统等操作。

Q3: 如何安全地加载第三方网页?

A:

javascript
// 创建隔离的 BrowserView 或 BrowserWindow
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
    webSecurity: true,
    // 不设置 preload
  }
})

// 设置导航限制
win.webContents.on('will-navigate', (event) => {
  event.preventDefault()
})

win.loadURL('https://trusted-site.com')

Q4: CSP 设置后部分功能不工作怎么办?

A: 检查浏览器控制台的 CSP 错误信息,逐步放宽限制:

html
<!-- 调试时可以先使用宽松策略 -->
<meta http-equiv="Content-Security-Policy" content="
  default-src 'self' 'unsafe-inline' 'unsafe-eval';
  connect-src 'self' *;
">

<!-- 生产环境逐步收紧 -->

Q5: 如何验证应用的安全性?

A: 使用以下工具和方法:

  1. Electron Security Warnings: 开发时会在控制台显示安全警告
  2. Electronegativity: Electron 应用安全审计工具
bash
npx @doyensec/electronegativity -i ./app
  1. 代码审计: 检查所有 BrowserWindow 配置
  2. 渗透测试: 尝试 XSS、路径遍历等攻击

Q6: contextBridge 暴露的 API 可以被篡改吗?

A: 不可以。contextBridge 暴露的值是只读的,渲染进程无法修改:

javascript
// preload.js
contextBridge.exposeInMainWorld('api', {
  secret: 'value'
})

// renderer.js
window.api.secret = 'hacked'  // 静默失败,值不变
console.log(window.api.secret)  // 仍然是 'value'
delete window.api.secret  // 静默失败

参考链接