Node.js vm2 沙箱完全教程:从入门到安全实践

vm2 是 Node.js 生态中最强大、安全的沙箱库,基于 Proxy 代理 与上下文隔离技术,在原生 vm 模块基础上构建了严密的安全边界,可安全执行不可信代码、防止沙箱逃逸,广泛用于插件系统、在线代码编辑器、模板引擎等场景。

本文从安装、基础使用、核心 API、高级配置、安全防护到实战案例,提供完整教程与可直接运行的详细代码。


一、环境准备与安装

1.1 版本说明

  • 最新稳定版:3.10.2(修复 CVE-2026-22709 Promise 逃逸高危漏洞)
  • 兼容:Node.js 12+

1.2 安装

bash

复制代码
# 项目内安装
npm install vm2 --save

# 全局安装(含 CLI)
npm install -g vm2

1.3 核心模块导入

vm2 提供 3 个核心类,按需导入:

javascript

复制代码
// CommonJS
const { VM, NodeVM, VMScript } = require('vm2');

// ES Module
import { VM, NodeVM, VMScript } from 'vm2';

二、基础沙箱:VM 类(轻量同步)

VM 是基础沙箱,无模块加载能力,适合执行简单同步代码,安全性最高、性能最好。

2.1 基础配置与运行代码

配置项(常用)
  • timeout:代码执行超时(ms),防死循环
  • allowAsync:是否允许异步代码(setTimeout/Promise),默认 false
  • sandbox:注入沙箱的全局变量(白名单)
  • console:是否允许 console,默认 false
  • eval:是否允许 eval/Function 构造器,默认 true
示例 1:简单计算与结果返回

javascript

复制代码
const { VM } = require('vm2');

// 1. 创建沙箱实例
const vm = new VM({
  timeout: 1000, // 1秒超时
  allowAsync: false,
  sandbox: {
    // 注入安全全局变量
    PI: 3.14159,
    add: (a, b) => a + b
  }
});

// 2. 运行代码(同步)
try {
  // 基础计算
  const res1 = vm.run('1 + 2 * 3');
  console.log('计算结果:', res1); // 输出:7

  // 使用注入变量
  const res2 = vm.run('add(PI, 1)');
  console.log('注入变量计算:', res2); // 输出:4.14159

  // 访问被禁止的全局对象(报错)
  vm.run('process.exit()');
} catch (err) {
  console.error('错误:', err.message);
  // 输出:ReferenceError: process is not defined
}
示例 2:自定义控制台输出

javascript

复制代码
const { VM } = require('vm2');

const vm = new VM({
  console: 'redirect', // 重定向控制台
  sandbox: {
    // 自定义console,仅允许log
    console: {
      log: (...args) => console.log('[沙箱日志]', ...args)
    }
  }
});

// 沙箱内console会被重定向
vm.run('console.log("Hello vm2!")');
// 输出:[沙箱日志] Hello vm2!

2.2 安全测试:原生 vm 对比 vm2

原生 vm 存在严重逃逸漏洞,vm2 可完美防护:

javascript

复制代码
// 原生 vm(危险)
const vm = require('vm');
const ctx = {};
vm.createContext(ctx);
// 逃逸代码:获取主进程process并终止
vm.runInContext('this.constructor.constructor("return process")().exit()', ctx);
// 进程直接被杀死!

// vm2(安全)
const { VM } = require('vm2');
const safeVM = new VM();
// 同样代码会被拦截
safeVM.run('this.constructor.constructor("return process")().exit()');
// 抛出:TypeError: constructor is not a function

三、高级沙箱:NodeVM 类(支持模块)

NodeVM 是增强版沙箱,支持 require 加载内置 / 外部模块,适合复杂业务场景(如插件)。

3.1 核心配置(require 控制)

require 配置是安全核心,遵循最小权限原则

javascript

复制代码
const { NodeVM } = require('vm2');

const nodeVM = new NodeVM({
  // 控制台继承主进程
  console: 'inherit',
  // 模块加载配置
  require: {
    // 允许的内置模块(白名单)
    builtin: ['fs', 'path', 'crypto'],
    // 允许加载外部模块(true/false/数组)
    external: true,
    // 外部模块根目录(限制访问范围)
    root: './sandbox_modules',
    // 禁止加载的模块
    deny: ['child_process', 'net'],
    // 模块映射(自定义别名)
    mapping: {
      'lodash': 'lodash-es'
    }
  },
  // 沙箱全局变量
  sandbox: {
    appConfig: { env: 'production' }
  }
});

3.2 示例 1:加载内置模块

javascript

复制代码
const { NodeVM } = require('vm2');

const vm = new NodeVM({
  console: 'inherit',
  require: {
    builtin: ['fs', 'path'] // 仅允许fs、path
  }
});

// 沙箱内读取文件(安全)
const code = `
  const fs = require('fs');
  const path = require('path');
  
  const filePath = path.join(__dirname, 'test.txt');
  fs.writeFileSync(filePath, 'vm2 write test');
  const content = fs.readFileSync(filePath, 'utf8');
  
  module.exports = content;
`;

const result = vm.run(code);
console.log('文件内容:', result); // 输出:vm2 write test

// 尝试加载禁止模块(报错)
vm.run('require("child_process")');
// 抛出:Error: Access denied to module child_process

3.3 示例 2:导出与调用沙箱函数

javascript

复制代码
const { NodeVM } = require('vm2');

const vm = new NodeVM({
  console: 'inherit',
  require: { external: false }
});

// 沙箱代码导出函数
const pluginCode = `
  module.exports = {
    greet: (name) => {
      console.log('Greet from sandbox');
      return \`Hello, \${name}\`;
    },
    calculate: (a, b) => a * b
  };
`;

// 运行并获取导出对象
const plugin = vm.run(pluginCode);

// 调用沙箱函数(主进程 ↔ 沙箱通信)
console.log(plugin.greet('Node.js')); // 输出:Hello, Node.js
console.log(plugin.calculate(5, 10)); // 输出:50

四、性能优化:VMScript 类(预编译)

VMScript预编译代码,重复执行时大幅提升性能(避免重复解析)。

4.1 基础用法

javascript

复制代码
const { VM, VMScript } = require('vm2');

// 1. 预编译脚本(编译一次)
const script = new VMScript(`
  let sum = 0;
  for(let i=0; i<10000; i++) sum += i;
  sum;
`, {
  filename: 'loop-script.js' // 调试文件名
});

// 2. 创建沙箱
const vm = new VM({ timeout: 500 });

// 3. 多次运行预编译脚本(高效)
console.time('执行10次');
for(let i=0; i<10; i++) {
  const res = vm.run(script);
  console.log(`第${i+1}次结果:`, res);
}
console.timeEnd('执行10次');

4.2 结合 NodeVM 预编译模块

javascript

复制代码
const { NodeVM, VMScript } = require('vm2');

// 预编译模块代码
const moduleScript = new VMScript(`
  const crypto = require('crypto');
  module.exports = (str) => crypto.createHash('md5').update(str).digest('hex');
`);

const vm = new NodeVM({
  require: { builtin: ['crypto'] }
});

// 运行预编译模块
const md5 = vm.run(moduleScript);
console.log('MD5:', md5('vm2-secure'));

五、安全机制与防护(核心)

5.1 vm2 三层安全架构

  1. 上下文隔离 :基于 vm.createContext 创建独立内存空间
  2. Proxy 代理防护:拦截所有对象访问、属性操作、函数调用
  3. 运行时监控:超时控制、错误捕获、危险 API 拦截

5.2 高危漏洞与修复

  • CVE-2026-22709 (<=3.10.1):Promise 回调净化缺陷 → 升级至 3.10.2+
  • 历史漏洞:原型链污染、错误对象逃逸 → 始终用最新版

5.3 安全最佳实践(必看)

  1. 最小权限 :只开放必要模块 / API,禁止 child_process/process
  2. 超时限制 :同步代码必设 timeout,异步代码额外监控
  3. 禁止危险函数eval: falsewasm: false
  4. 范围限制 :外部模块设 root,文件操作限制目录
  5. 版本锁定:固定 vm2 版本,定期更新安全补丁
  6. 深度防御:结合进程隔离(如 worker_threads)、权限控制

5.4 安全配置模板(生产可用)

javascript

复制代码
const { NodeVM } = require('vm2');

const secureVM = new NodeVM({
  // 基础安全
  timeout: 3000,
  eval: false,
  wasm: false,
  allowAsync: true,
  // 控制台
  console: 'redirect',
  sandbox: {
    console: {
      log: (...args) => console.log('[SANDBOX]', ...args),
      warn: (...args) => console.warn('[SANDBOX]', ...args)
    }
  },
  // 严格模块控制
  require: {
    builtin: ['path', 'crypto', 'buffer'], // 最小内置模块
    external: false, // 禁止外部模块
    deny: ['fs', 'os', 'process', 'child_process'], // 黑名单
    root: './' // 限制根目录
  },
  // 错误捕获
  onCancel: () => console.error('沙箱代码超时被终止')
});

六、实战案例:安全插件系统

6.1 需求

  • 主程序加载第三方插件
  • 插件仅能访问指定 API
  • 防止插件逃逸、破坏主进程

6.2 完整代码

javascript

复制代码
const { NodeVM } = require('vm2');
const fs = require('fs');
const path = require('path');

// 1. 安全沙箱工厂
function createPluginSandbox(pluginName) {
  return new NodeVM({
    console: 'inherit',
    timeout: 2000,
    eval: false,
    require: {
      builtin: ['path'],
      external: false
    },
    // 注入插件可用API(白名单)
    sandbox: {
      pluginName,
      // 安全工具函数
      utils: {
        formatDate: (date) => new Date(date).toLocaleString(),
        random: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min
      },
      // 主程序提供的服务
      pluginService: {
        log: (msg) => console.log(`[插件${pluginName}]`, msg),
        getData: () => ({ user: 'admin', role: 'guest' })
      }
    }
  });
}

// 2. 加载并执行插件
function loadPlugin(pluginPath) {
  try {
    const pluginCode = fs.readFileSync(pluginPath, 'utf8');
    const pluginName = path.basename(pluginPath, '.js');
    const vm = createPluginSandbox(pluginName);
    const plugin = vm.run(pluginCode);
    
    // 初始化插件
    if (typeof plugin.init === 'function') {
      plugin.init();
    }
    
    console.log(`插件${pluginName}加载成功`);
    return plugin;
  } catch (err) {
    console.error('插件加载失败:', err.message);
    return null;
  }
}

// 3. 插件代码(sandbox_plugins/demo-plugin.js)
/*
module.exports = {
  init() {
    pluginService.log('插件初始化');
    const data = pluginService.getData();
    pluginService.log(`用户数据: ${JSON.stringify(data)}`);
    const date = utils.formatDate(Date.now());
    pluginService.log(`当前时间: ${date}`);
    const num = utils.random(1, 100);
    pluginService.log(`随机数: ${num}`);
  },
  execute: (params) => {
    pluginService.log(`执行任务: ${params}`);
    return `插件处理结果: ${params.toUpperCase()}`;
  }
};
*/

// 4. 运行
const plugin = loadPlugin(path.join(__dirname, 'sandbox_plugins/demo-plugin.js'));
if (plugin) {
  const result = plugin.execute('vm2-plugin-test');
  console.log('主程序接收:', result);
}

七、常见问题与解决方案

7.1 沙箱内无法访问主进程变量

  • 原因:默认完全隔离,需通过 sandbox 显式注入
  • 解决:配置 sandbox 白名单,仅传递必要对象

7.2 异步代码超时不生效

  • 原因:timeout 仅对同步代码有效
  • 解决:异步代码结合 Promise.race 手动超时:

javascript

复制代码
const vm = new VM({ allowAsync: true });
const code = 'new Promise(resolve => setTimeout(() => resolve("done"), 5000))';
// 手动超时控制
Promise.race([
  vm.run(code),
  new Promise((_, reject) => setTimeout(() => reject(new Error('异步超时')), 2000))
]);

7.3 沙箱逃逸风险

  • 解决:
    1. 升级至最新版 vm2
    2. 严格限制模块与 API
    3. 结合进程隔离(如 worker_threads
    4. 生产环境禁用高危功能

八、总结

vm2 是 Node.js 安全执行不可信代码的首选方案,VM 适合轻量同步场景,NodeVM 支持模块加载满足复杂需求,VMScript 优化重复执行性能。

核心原则最小权限 + 最新版本 + 深度防御,即可构建高安全的沙箱环境。

相关推荐
星光不问赶路人1 天前
Node.js 如何判断入口文件:从 require.main 到 ES Module 实现
前端·node.js
网络点点滴1 天前
Node.js 中阻塞、非阻塞及异步特性
node.js
netkiller-BG7NYT1 天前
yoloutils - Openclaw Agent Skill
前端·webpack·node.js
cypking1 天前
npm 依赖包版本扫描提示插件Version Lens
前端·npm·node.js
研究点啥好呢2 天前
Github热门项目推荐 | 创建你的像素风格!
c++·python·node.js·github·开源软件
孟祥_成都2 天前
复刻字节 AI 开发流:实践 Node.js 通用脚手架
前端·人工智能·node.js
BLUcoding2 天前
NVM for Windows 管理 Node.js 多版本
node.js
爱学习的程序媛2 天前
Node.js 异步任务协作:7 种实用方案与真实项目案例
node.js·异步编程
KevinCyao2 天前
node.js视频短信接口如何接入?使用异步非阻塞模式下发视频短信API
node.js
ZHANG13HAO2 天前
Python 调用 Node.js(vm2 沙箱)完美方案:胶水层实战教程
开发语言·python·node.js