{T}

Electron 应用安全性指南

本文档从应用安全性和源码安全性两个维度,详细介绍 Electron 应用的安全最佳实践。遵循这些指南可以有效防范常见安全威胁,保护应用和用户数据安全。

前言:Electron 的安全挑战

Electron 通过将 Chromium 和 Node.js 结合在一起,使得开发者能用 Web 技术构建桌面应用。然而,这种结合也带来了独特的安全挑战:

code
┌─────────────────────────────────────────────────────────────┐
│                    Electron 安全风险矩阵                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Chromium (渲染进程)          Node.js (主进程)               │
│  ┌─────────────────┐         ┌─────────────────┐           │
│  │ • XSS 攻击       │   IPC   │ • 文件系统访问   │           │
│  │ • 恶意网页       │ ──────► │ • 系统命令执行   │           │
│  │ • 远程代码注入   │         │ • 进程管理       │           │
│  │ • 数据泄露       │         │ • 网络请求       │           │
│  └─────────────────┘         └─────────────────┘           │
│                                                             │
│  风险:渲染进程被攻破 → 通过 Node.js 获得系统控制权           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

核心风险:如果渲染进程被恶意代码利用,可能通过 Node.js 的能力访问操作系统,导致严重的安全漏洞,如:

漏洞类型危害等级潜在后果
远程代码执行(RCE)严重攻击者可执行任意系统命令
本地文件读取敏感数据泄露
路径遍历访问预期外的文件
原型污染中高绕过安全检查

应用安全性

应用安全的核心思想是权限最小化,即限制渲染进程访问 Node.js 和操作系统资源的能力。

1. 开启 contextIsolation(上下文隔离)

contextIsolation 是 Electron 最重要的安全特性之一。它确保预加载脚本(preload.js)和渲染器进程的 Web 内容运行在不同的 JavaScript 上下文中。

版本信息

Electron 版本默认值
< v12false
>= v12true

配置方式

javascript
// main.js
const mainWindow = new BrowserWindow({
  webPreferences: {
    // preload 是必须的,用于连接主进程和渲染进程
    preload: path.join(app.getAppPath(), 'preload.js')
    // contextIsolation 默认为 true,无需显式设置
    // contextIsolation: true,
  }
})

预加载脚本安全暴露 API

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

// ✅ 正确:通过 contextBridge 安全地暴露 API
contextBridge.exposeInMainWorld('myAPI', {
  // 暴露特定方法
  doAThing: () => ipcRenderer.send('do-a-thing'),
  
  // 双向通信(返回 Promise)
  loadPreferences: () => ipcRenderer.invoke('load-prefs'),
  
  // 带参数的方法
  saveFile: (content) => ipcRenderer.invoke('file:save', content),
  
  // 事件监听
  onUpdate: (callback) => {
    ipcRenderer.on('update-available', (event, info) => callback(info))
  }
})

// ❌ 错误:直接暴露整个 ipcRenderer(危险!)
// contextBridge.exposeInMainWorld('ipc', ipcRenderer)

渲染进程使用

javascript
// renderer.js
// 只能调用暴露的方法,无法访问 Node.js 或 Electron 内部 API
async function init() {
  const prefs = await window.myAPI.loadPreferences()
  console.log('Preferences:', prefs)
}

window.myAPI.onUpdate((info) => {
  console.log('Update available:', info)
})

安全效果对比

code
┌─────────────────────────────────────────────────────────────┐
│           contextIsolation: false (不安全)                   │
├─────────────────────────────────────────────────────────────┤
│  Preload Context (共享)                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ window.secret = 'sensitive-data'                    │    │
│  │ window.electron = require('electron')               │    │
│  └─────────────────────────────────────────────────────┘    │
│                          │                                   │
│                          │ 可互相访问                        │
│                          ▼                                   │
│  Renderer Context                                           │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ // 恶意代码可以:                                     │    │
│  │ console.log(window.secret)  // 读取敏感数据          │    │
│  │ window.electron.ipcRenderer.send(...) // 完全控制    │    │
│  │ window.__proto__.polluted = true  // 原型污染        │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│           contextIsolation: true (安全)                      │
├─────────────────────────────────────────────────────────────┤
│  Preload Context (隔离)                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ const secret = 'sensitive-data'                     │    │
│  │ const electron = require('electron')                │    │
│  │ // 通过 contextBridge 暴露有限 API                   │    │
│  └─────────────────────────────────────────────────────┘    │
│                          │                                   │
│                          │ 只读代理                          │
│                          ▼                                   │
│  Renderer Context                                           │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ window.myAPI.saveFile()  // ✓ 只能用暴露的 API       │    │
│  │ console.log(secret)       // ✗ undefined            │    │
│  │ window.myAPI = hacked     // ✗ 无法修改              │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

2. 开启 sandbox(沙箱模式)

沙箱化进一步隔离渲染进程,使其在一个受限的环境中运行,即使在 contextIsolation 被绕过的情况下,也能限制其对操作系统的访问。

版本信息

Electron 版本默认值
< v20false
>= v20true

配置方式

javascript
// main.js
const mainWindow = new BrowserWindow({
  webPreferences: {
    sandbox: true,  // 默认为 true(Electron v20+)
    preload: path.join(app.getAppPath(), 'preload.js')
  }
})

沙箱对预加载脚本的影响

javascript
// preload.js (sandbox: true 时)

// ✅ 可用
const { contextBridge, ipcRenderer } = require('electron')

// ✅ 可用:有限的 process 对象
console.log(process.platform)  // 'darwin', 'win32', etc.
console.log(process.type)      // 'renderer'

// ❌ 不可用:文件系统
// const fs = require('fs')  // Error

// ❌ 不可用:子进程
// const { exec } = require('child_process')  // Error

// ❌ 不可用:原生模块
// const native = require('./native.node')  // Error

// 正确做法:通过 IPC 让主进程处理
contextBridge.exposeInMainWorld('fs', {
  readFile: (path) => ipcRenderer.invoke('fs:read', path),
  writeFile: (path, content) => ipcRenderer.invoke('fs:write', path, content)
})

沙箱架构示意

code
┌─────────────────────────────────────────────────────────────┐
│                    沙箱进程 (sandbox: true)                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                    Chromium 沙箱                     │    │
│  │  ┌─────────────────────────────────────────────┐    │    │
│  │  │ 渲染进程                                     │    │    │
│  │  │  • 无文件系统访问                            │    │    │
│  │  │  • 无网络原始套接字                          │    │    │
│  │  │  • 无进程创建能力                            │    │    │
│  │  │  • 受限的系统调用                            │    │    │
│  │  └─────────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
│                          │                                   │
│                          │ IPC (受限)                        │
│                          ▼                                   │
│  ┌─────────────────────────────────────────────────────┐    │
│  │                    主进程                            │    │
│  │  • 完整 Node.js 能力                                │    │
│  │  • 处理文件、网络、进程等操作                        │    │
│  │  • 验证所有来自沙箱的请求                            │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

注意:当 sandbox: true 时,预加载脚本中的 Node.js 能力受限。所有文件系统、网络等操作需通过 IPC 由主进程处理。


3. 禁用 nodeIntegration

nodeIntegration 允许渲染进程直接访问 Node.js API。这是一个非常危险的设置,应始终禁用。

版本信息

Electron 版本默认值
所有版本false

危险配置示例

javascript
// ❌ 极度危险配置!
const mainWindow = new BrowserWindow({
  webPreferences: {
    nodeIntegration: true,      // 允许渲染进程访问 Node.js
    contextIsolation: false,    // 无上下文隔离
    webSecurity: false          // 禁用同源策略
  }
})

mainWindow.loadURL('https://example.com')

// 如果 example.com 被攻破或包含恶意脚本:
// <script>
//   const { exec } = require('child_process')
//   exec('rm -rf ~/Documents')  // 删除用户文件
//   const fs = require('fs')
//   const password = fs.readFileSync('/etc/passwd')  // 读取系统文件
// </script>

安全配置

javascript
// ✅ 安全配置
const mainWindow = new BrowserWindow({
  webPreferences: {
    nodeIntegration: false,      // 保持默认
    contextIsolation: true,      // 启用上下文隔离
    sandbox: true,               // 启用沙箱
    preload: path.join(__dirname, 'preload.js')
  }
})

禁用 nodeIntegration 的理由

如果启用后果
可访问 fs 模块读写任意文件
可访问 child_process执行任意系统命令
可访问 net 模块绕过防火墙
可访问 os 模块获取系统信息
加载远程内容时远程代码执行(RCE)

4. 确保 webSecurity 开启

webSecurity 强制执行同源策略,阻止跨站脚本(XSS)攻击。

版本信息

Electron 版本默认值
所有版本true

错误做法

javascript
// ❌ 禁用 webSecurity 会带来严重风险
const mainWindow = new BrowserWindow({
  webPreferences: {
    webSecurity: false  // 危险!
  }
})

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

正确处理跨域

javascript
// ✅ 使用主进程代理跨域请求
// main.js
const { ipcMain } = require('electron')

ipcMain.handle('http:fetch', async (event, url, options) => {
  // 验证 URL
  const allowedDomains = ['api.example.com', 'cdn.example.com']
  const urlObj = new URL(url)
  
  if (!allowedDomains.includes(urlObj.hostname)) {
    throw new Error('Domain not allowed')
  }
  
  const response = await fetch(url, options)
  return {
    ok: response.ok,
    status: response.status,
    data: await response.json()
  }
})

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

// renderer.js
const data = await window.http.fetch('https://api.example.com/data')

修改响应头示例

javascript
// main.js - 设置 CSP 和 CORS
mainWindow.webContents.session.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ["default-src 'self'"],
      'Access-Control-Allow-Origin': ['https://trusted-domain.com']
    }
  })
})

5. 限制导航与新窗口创建

防止应用意外导航到恶意网站或打开非预期的窗口。

导航限制

javascript
// main.js
const { app } = require('electron')

// 全局导航控制
app.on('web-contents-created', (event, contents) => {
  // 阻止所有非白名单的导航
  contents.on('will-navigate', (event, navigationUrl) => {
    const parsedUrl = new URL(navigationUrl)
    
    // 定义允许的源
    const allowedOrigins = [
      'https://myapp.com',
      'https://docs.myapp.com'
    ]
    
    if (!allowedOrigins.includes(parsedUrl.origin)) {
      console.warn(`Blocked navigation to: ${navigationUrl}`)
      event.preventDefault()
    }
  })
  
  // 防止页面通过 JavaScript 跳转
  contents.on('will-redirect', (event, url) => {
    // 应用相同的安全策略
    const parsedUrl = new URL(url)
    if (parsedUrl.origin !== 'https://myapp.com') {
      event.preventDefault()
    }
  })
})

新窗口控制

javascript
// main.js
const { shell } = require('electron')

app.on('web-contents-created', (event, contents) => {
  // 控制新窗口打开行为
  contents.setWindowOpenHandler(({ url, frameName, disposition }) => {
    // 外部链接在默认浏览器中打开
    if (url.startsWith('http://') || url.startsWith('https://')) {
      shell.openExternal(url)
      return { action: 'deny' }
    }
    
    // 特定的内部页面可以打开新窗口
    if (url.startsWith('app://')) {
      return {
        action: 'allow',
        overrideBrowserWindowOptions: {
          webPreferences: {
            contextIsolation: true,
            nodeIntegration: false,
            sandbox: true
          }
        }
      }
    }
    
    // 其他请求拒绝
    return { action: 'deny' }
  })
})

iframe 安全控制

javascript
// 阻止 iframe 加载不可信内容
contents.on('will-attach-webview', (event, webPreferences, params) => {
  // 移除 preload
  delete webPreferences.preload
  
  // 禁用 Node.js
  webPreferences.nodeIntegration = false
  webPreferences.contextIsolation = true
  
  // 验证 src
  if (!params.src.startsWith('https://trusted-domain.com')) {
    event.preventDefault()
  }
})

6. 校验与过滤 IPC 通信

所有从渲染进程到主进程的 IPC 消息都应被视为不可信的,必须进行严格验证。

Preload 脚本白名单

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

// 定义允许的通道
const ALLOWED_CHANNELS = {
  invoke: ['app:version', 'file:read', 'file:write', 'dialog:open'],
  send: ['window:close', 'window:minimize', 'window:maximize'],
  receive: ['update:available', 'update:downloaded', 'notification:show']
}

function checkChannel(type, channel) {
  if (!ALLOWED_CHANNELS[type]?.includes(channel)) {
    console.error(`IPC channel "${channel}" not in ${type} whitelist`)
    return false
  }
  return true
}

contextBridge.exposeInMainWorld('electron', {
  invoke: (channel, ...args) => {
    if (!checkChannel('invoke', channel)) {
      return Promise.reject(new Error('Channel not allowed'))
    }
    return ipcRenderer.invoke(channel, ...args)
  },
  
  send: (channel, ...args) => {
    if (!checkChannel('send', channel)) {
      return
    }
    ipcRenderer.send(channel, ...args)
  },
  
  on: (channel, callback) => {
    if (!checkChannel('receive', channel)) {
      return () => {}
    }
    const handler = (event, ...args) => callback(...args)
    ipcRenderer.on(channel, handler)
    return () => ipcRenderer.removeListener(channel, handler)
  }
})

主进程参数验证

javascript
// main.js
const { ipcMain, app, dialog } = require('electron')
const path = require('path')
const fs = require('fs')
const { z } = require('zod')  // 可选:使用 zod 进行 schema 验证

// 定义允许的目录
const ALLOWED_DIRS = {
  documents: app.getPath('documents'),
  downloads: app.getPath('downloads'),
  userData: app.getPath('userData')
}

// 验证路径
function validatePath(userPath, allowedDir) {
  const resolved = path.resolve(allowedDir, userPath)
  if (!resolved.startsWith(allowedDir)) {
    throw new Error('Path traversal attempt blocked')
  }
  return resolved
}

// 使用 schema 验证(推荐)
const readFileSchema = z.object({
  path: z.string().min(1).max(1024),
  encoding: z.enum(['utf-8', 'binary', 'base64']).optional()
})

ipcMain.handle('file:read', async (event, params) => {
  // 1. Schema 验证
  const validated = readFileSchema.parse(params)
  
  // 2. 路径安全检查
  const safePath = validatePath(validated.path, ALLOWED_DIRS.userData)
  
  // 3. 执行操作
  const content = await fs.promises.readFile(safePath, validated.encoding || 'utf-8')
  return content
})

// 手动验证示例
ipcMain.handle('dialog:open', async (event, options) => {
  // 验证参数类型
  if (options && typeof options !== 'object') {
    throw new TypeError('options must be an object')
  }
  
  // 验证具体字段
  if (options?.title && typeof options.title !== 'string') {
    throw new TypeError('title must be a string')
  }
  
  // 使用安全的默认值
  const result = await dialog.showOpenDialog({
    title: options?.title || '选择文件',
    defaultPath: ALLOWED_DIRS.documents,
    filters: options?.filters || [{ name: 'All Files', extensions: ['*'] }],
    properties: ['openFile']
  })
  
  return result.filePaths
})

IPC 安全流程

code
┌─────────────────────────────────────────────────────────────┐
│                    IPC 安全验证流程                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  渲染进程                                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ window.electron.invoke('file:read', { path: '...' })│    │
│  └─────────────────────────────────────────────────────┘    │
│                          │                                   │
│                          ▼                                   │
│  Preload 脚本                                               │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 1. 检查通道白名单                                     │    │
│  │ 2. 检查参数类型(基础)                               │    │
│  │ 3. 转发到主进程                                       │    │
│  └─────────────────────────────────────────────────────┘    │
│                          │                                   │
│                          ▼                                   │
│  主进程                                                     │
│  ┌─────────────────────────────────────────────────────┐    │
│  │ 1. 验证发送者身份                                     │    │
│  │ 2. Schema/类型验证                                    │    │
│  │ 3. 路径/URL 安全检查                                  │    │
│  │ 4. 权限检查                                           │    │
│  │ 5. 执行操作                                           │    │
│  │ 6. 返回结果(过滤敏感信息)                            │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

7. 内容安全策略

内容安全策略(CSP)是防止 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;
  ">
</head>
<body>
  <!-- 应用内容 -->
</body>
</html>

方式二:主进程设置

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

session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': [
        "default-src 'self'; " +
        "script-src 'self'; " +
        "style-src 'self' 'unsafe-inline'; " +
        "connect-src 'self' https://api.example.com;"
      ]
    }
  })
})

常用 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';
">

<!-- 开发模式 -->
<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 -->
<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;
">

源码安全性

源码安全的目标是增加攻击者逆向工程和篡改代码的难度。以下是几种常见的源码保护策略:

1. asar 文件防护

默认情况下,Electron 将应用源码打包成 app.asar 文件。这是一个简单的归档文件,可以被轻松解压。

风险分析

bash
# 攻击者可以轻松解压 asar
npx asar extract app.asar ./extracted

# 解压后可修改代码(例如绕过授权检查)
# 然后重新打包
npx asar pack ./extracted app.asar

防护措施

方式一:asar 完整性校验

javascript
// electron-builder 配置
// electron-builder.yml
asar: true
asarUnpack:
  - "**/*.node"  # 原生模块不打包

// 启用 asar 完整性校验(需要 electron-builder 23+)
// package.json
{
  "build": {
    "asar": true,
    "asarSign": {
      "algorithm": "sha256"
    }
  }
}

方式二:启动时校验

javascript
// main.js
const crypto = require('crypto')
const fs = require('fs')
const path = require('path')

function verifyIntegrity() {
  const expectedHash = 'your-expected-sha256-hash'
  const asarPath = path.join(process.resourcesPath, 'app.asar')
  
  if (fs.existsSync(asarPath)) {
    const content = fs.readFileSync(asarPath)
    const hash = crypto.createHash('sha256').update(content).digest('hex')
    
    if (hash !== expectedHash) {
      dialog.showErrorBox('安全警告', '应用文件已被篡改')
      app.quit()
    }
  }
}

app.on('ready', verifyIntegrity)

2. JavaScript 代码压缩与混淆

压缩和混淆可以减小代码体积,并极大地增加逆向工程的难度。

使用 Terser 压缩

javascript
// vite.config.js 或 webpack.config.js
import { defineConfig } from 'vite'
import terser from '@rollup/plugin-terser'

export default defineConfig({
  build: {
    minify: 'terser',
    terserOptions: {
      compress: {
        drop_console: true,  // 移除 console
        drop_debugger: true, // 移除 debugger
        pure_funcs: ['console.log']
      },
      mangle: {
        properties: {
          regex: /^_/  // 混淆以 _ 开头的属性
        }
      }
    }
  }
})

使用 javascript-obfuscator 混淆

javascript
// vite.config.js
import obfuscator from 'rollup-plugin-obfuscator'

export default defineConfig({
  plugins: [
    obfuscator({
      options: {
        compact: true,
        controlFlowFlattening: true,       // 控制流平坦化
        deadCodeInjection: true,           // 注入死代码
        debugProtection: true,             // 禁用调试
        disableConsoleOutput: true,        // 禁用控制台
        stringArray: true,                 // 字符串数组化
        stringArrayEncoding: ['base64'],   // 字符串编码
        transformObjectKeys: true          // 转换对象键
      }
    })
  ]
})

性能注意事项

code
┌─────────────────────────────────────────────────────────────┐
│                    混淆级别与性能权衡                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  低混淆 ◄────────────────────────────────────► 高混淆       │
│                                                             │
│  • 启动快                    • 启动慢                        │
│  • 体积小                    • 体积大                        │
│  • 易逆向                    • 难逆向                        │
│  • 调试方便                  • 调试困难                      │
│                                                             │
│  推荐:仅对核心敏感代码使用高混淆                             │
│                                                             │
└─────────────────────────────────────────────────────────────┘

3. 核心逻辑原生模块加密

对于最核心的算法或商业逻辑,可以将其用 C++ 或 Rust 编写成 Node.js 原生模块。

开发流程

code
┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  C++ / Rust  │ ──►│   编译工具    │ ──►│   .node 文件 │
│   源代码     │    │  node-gyp    │    │  (二进制)     │
└──────────────┘    │   N-API      │    └──────────────┘
                    │   Neon       │             │
                    └──────────────┘             │
                                                 ▼
                    ┌──────────────┐    ┌──────────────┐
                    │  JavaScript  │ ◄──│   加载模块    │
                    │   应用代码    │    │   require()  │
                    └──────────────┘    └──────────────┘

C++ 原生模块示例

cpp
// native.cc
#include <node_api.h>
#include <string>

// 核心加密逻辑
std::string encryptData(const std::string& data, const std::string& key) {
    // 实现加密算法
    // ...
    return encrypted;
}

// Node.js 绑定
static napi_value Encrypt(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2];
    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);
    
    // 获取参数...
    // 调用加密函数...
    // 返回结果...
    
    return result;
}

static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc = { "encrypt", nullptr, Encrypt, nullptr, nullptr, nullptr, napi_default, nullptr };
    napi_define_properties(env, exports, 1, &desc);
    return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)
javascript
// 使用原生模块
const native = require('./build/Release/native.node')

const encrypted = native.encrypt('sensitive-data', 'secret-key')

Rust 原生模块示例(使用 Neon)

rust
// src/lib.rs
use neon::prelude::*;

fn encrypt(mut cx: FunctionContext) -> JsResult<JsString> {
    let data = cx.argument::<JsString>(0)?.value(&mut cx);
    let key = cx.argument::<JsString>(1)?.value(&mut cx);
    
    // 实现加密逻辑
    let encrypted = format!("encrypted:{}:{}", key, data);
    
    Ok(cx.string(encrypted))
}

#[neon::main]
fn main(mut cx: ModuleContext) -> NeonResult<()> {
    cx.export_function("encrypt", encrypt)?;
    Ok(())
}

4. 使用 V8 字节码

V8 字节码将 JavaScript 源码编译成 V8 引擎可直接执行的字节码格式,应用中不包含可读的源码。

使用 electron-vite

javascript
// electron.vite.config.ts
import { defineConfig, bytecodePlugin } from 'electron-vite'

export default defineConfig({
  main: {
    plugins: [bytecodePlugin({
      // 字节码配置
      chunkAlias: 'main'  // 指定需要编译的 chunk
    })]
  },
  preload: {
    plugins: [bytecodePlugin()]
  },
  renderer: {
    // 渲染进程通常不需要字节码
    // 因为代码可能需要动态执行
  }
})

使用 bytenode

javascript
// 编译脚本 compile.js
const bytenode = require('bytenode')
const path = require('path')

async function compile() {
  // 编译主进程代码
  await bytenode.compileFile({
    filename: path.join(__dirname, 'main.js'),
    output: path.join(__dirname, 'main.jsc')
  })
  
  // 创建入口文件
  const entry = `require('bytenode');
require('./main.jsc');`
  
  require('fs').writeFileSync(path.join(__dirname, 'entry.js'), entry)
}

compile()

字节码优势

特性说明
无源码最终交付不包含可读 JavaScript
启动快省去 V8 解析编译步骤
难逆向字节码比混淆代码更难分析

注意事项

javascript
// ⚠️ 字节码不支持的功能
// 1. 动态代码执行
eval(code)              // ❌ 不支持
new Function(code)      // ❌ 不支持

// 2. 动态导入(需要特殊处理)
import(moduleName)      // ❌ 可能不支持

// 3. 跨版本兼容
// V8 字节码与 V8 版本绑定
// 不同 Electron 版本可能不兼容

安全配置速查表

webPreferences 配置

javascript
const secureConfig = {
  // === 必须配置 ===
  contextIsolation: true,              // ✅ 必须启用
  nodeIntegration: false,              // ✅ 必须禁用
  webSecurity: true,                   // ✅ 必须启用
  enableRemoteModule: false,           // ✅ 必须禁用
  allowRunningInsecureContent: false,  // ✅ 必须禁用
  
  // === 推荐配置 ===
  sandbox: true,                       // ✅ 推荐启用
  nodeIntegrationInWorker: false,      // ✅ 推荐禁用
  nodeIntegrationInSubFrames: false,   // ✅ 推荐禁用
  
  // === 预加载脚本 ===
  preload: path.join(__dirname, 'preload.js')
}

安全配置检查脚本

javascript
// security-check.js
const { BrowserWindow } = require('electron')

function checkWindowSecurity(win) {
  const prefs = win.webContents.getLastWebPreferences()
  const issues = []
  
  if (!prefs.contextIsolation) {
    issues.push('❌ contextIsolation is disabled')
  }
  if (prefs.nodeIntegration) {
    issues.push('❌ nodeIntegration is enabled')
  }
  if (!prefs.webSecurity) {
    issues.push('❌ webSecurity is disabled')
  }
  if (prefs.enableRemoteModule) {
    issues.push('❌ enableRemoteModule is enabled')
  }
  
  return issues.length === 0 ? '✅ All security checks passed' : issues
}

// 在创建窗口后调用
app.on('browser-window-created', (event, win) => {
  console.log('Security check:', checkWindowSecurity(win))
})

常见问题解答

Q1: 如何在禁用 nodeIntegration 后访问 Node.js 功能?

A: 通过 preload 脚本和 contextBridge 安全暴露 API:

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

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

Q2: sandbox 模式下如何进行文件操作?

A: 文件操作必须通过 IPC 由主进程执行:

javascript
// preload.js
contextBridge.exposeInMainWorld('fs', {
  read: (path) => ipcRenderer.invoke('fs:read', path)
})

// main.js
ipcMain.handle('fs:read', async (event, path) => {
  // 验证路径...
  return fs.readFileSync(path, 'utf-8')
})

Q3: 如何调试被混淆的代码?

A: 开发环境不混淆,仅生产环境混淆:

javascript
// vite.config.js
export default defineConfig(({ mode }) => ({
  plugins: mode === 'production' ? [obfuscator()] : []
}))

Q4: CSP 导致某些功能不工作怎么办?

A: 查看控制台错误,逐步放宽限制:

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

<!-- 确定需要的资源后收紧策略 -->

Q5: 如何保护敏感数据(如 API 密钥)?

A: 不要在渲染进程存储敏感数据:

javascript
// ❌ 错误:在渲染进程存储
// localStorage.setItem('apiKey', 'secret-key')

// ✅ 正确:在主进程存储
// main.js
const keytar = require('keytar')

ipcMain.handle('api:getKey', async () => {
  return keytar.getPassword('myapp', 'api-key')
})

ipcMain.handle('api:setKey', async (event, key) => {
  return keytar.setPassword('myapp', 'api-key', key)
})

Q6: 如何检测应用是否被篡改?

A: 实现完整性校验:

javascript
const crypto = require('crypto')
const fs = require('fs')

function checkIntegrity() {
  const knownHash = 'your-precomputed-hash'
  const asarPath = `${process.resourcesPath}/app.asar`
  
  if (fs.existsSync(asarPath)) {
    const hash = crypto
      .createHash('sha256')
      .update(fs.readFileSync(asarPath))
      .digest('hex')
    
    if (hash !== knownHash) {
      // 应用被篡改
      app.quit()
    }
  }
}

总结与延伸阅读

Electron 应用的安全性是一个系统工程,需要从应用层和源码层两个维度进行综合防护。开发者应始终保持警惕,遵循最新的安全最佳实践。

核心原则

原则说明
最小权限渲染进程的权限越小越好
深度防御同时采用多种安全策略
输入验证所有来自渲染进程的数据都不可信
源码保护增加逆向工程和代码篡改的成本

安全配置清单

markdown
必须项:
- [x] contextIsolation: true
- [x] nodeIntegration: false
- [x] webSecurity: true
- [x] enableRemoteModule: false
- [x] 使用 contextBridge
- [x] IPC 通道白名单
- [x] 设置 CSP

推荐项:
- [ ] sandbox: true
- [ ] IPC 参数验证
- [ ] 路径安全检查
- [ ] 代码混淆
- [ ] asar 完整性校验

延伸阅读