在一些系统中,咱们但愿给用户提供插入自定义逻辑的能力,除了 RPC
和 REST
以外,运行客户提供的代码也是比较经常使用的方法,好处是能够极大地减小在网络上的耗时。JavaScript 是一种很是流行并且容易上手的语言,所以,让用户用 JavaScript 来写自定义逻辑是一个不错的选择。下面咱们介绍 Node.js 提供的 vm 模块以及分析用它来运行不信任代码可能遇到的问题。php
vm 模块是 Node.js 内置的核心模块,它能让咱们编译 JavaScript 代码和在指定的环境中运行。请看下面例子:html
const util = require('util'); const vm = require('vm'); // 1. 建立一个 vm.Script 实例, 编译要执行的代码 const script = new vm.Script('globalVar += 1; anotherGlobalVar = 1; '); // 2. 用于绑定到 context 的对象 const sandbox = {globalVar: 1}; // 3. 建立一个 context, 而且把 sandbox 这个对象绑定到这个环境, 做为全局对象 const contextifiedSandbox = vm.createContext(sandbox); // 4. 运行上面编译的代码, context 是 contextifiedSandbox const result = script.runInContext(contextifiedSandbox); console.log(`sandbox === contextifiedSandbox ? ${sandbox === contextifiedSandbox}`); // sandbox === contextifiedSandbox ? true console.log(`sandbox: ${util.inspect(sandbox)}`); // sandbox: { globalVar: 2, anotherGlobalVar: 1 } console.log(`result: ${util.inspect(result)}`); // result: 1
vm.Script
是一个类,用于建立代码实例,后面能够屡次运行。node
vm.createContext(sandbox)
用于 "contextify" 一个对象,根据 ECMAScript 2015 语言规范,代码的执行须要一个 execution context。这里的 "contextify",就是把传进去的对象与 V8 的一个新的 context 进行关联。这里所说的关联,个人理解是,这个 "contextified" 对象的属性将会成为那个 context 的全局属性,同时,在 context 下运行代码时产生的全局属性也会成为这个 "contextified" 对象的属性。linux
script.runInContext(contextifiedSandbox)
就是使代码在 contextifiedSandbox
这个 context 中运行,从上面的输出能够看到,代码运行后,contextifiedSandbox
里面的属性的值已经被改变了,运行结果是最后一个表达式的值。 git
除了上面几个接口以外,vm 模块还有一些更便捷的接口,例如 vm.runInContext(code, contextifiedSandbox[, options])
,vm.runInNewContext(code[, sandbox][, options])
等,详细可看文档。github
咱们用 vm 运行代码的时候极可能须要获得一些结果,从上面的例子中能够看到,咱们能够经过把结果做为最后一个表达式的值传给外层,或者做为context
的属性给外层使用,这在同步代码里没有问题,可是假如结果须要依赖里面的异步操做呢?这时,咱们能够经过在 context
里放一个回调函数。 下面是例子:api
const util = require('util'); const vm = require('vm'); const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) { console.log(result); }}; vm.createContext(sandbox); const script = new vm.Script(` setTimeout(function(){ globalVar++; cb("async result"); }, 1000); `,{}); script.runInContext(sandbox); console.log(`globalVar: ${sandbox.globalVar}`); // globalVar: 1 // async result
script.runInContext(contextifiedSandbox[, options])
方法有一个 timeout
选项能够设定代码的运行时间,若是超过期间就会抛出错误,请看下面例子: 安全
const util = require('util'); const vm = require('vm'); const sandbox = {}; const contextifiedSandbox = vm.createContext(sandbox); const script = new vm.Script('while(true){}'); const result = script.runInContext(contextifiedSandbox, {timeout: 1000}); // const result = script.runInContext(contextifiedSandbox, {timeout: 1000}); // ^ // Error: Script execution timed out.
再试试异步代码,网络
const util = require('util'); const vm = require('vm'); const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) { console.log(result); }}; vm.createContext(sandbox); const script = new vm.Script(` setTimeout(function(){ globalVar++; cb("async result"); }, 1000); globalVar; `,{}); const result = script.runInContext(sandbox, {timeout: 500}); console.log(`result: ${result}`); // result: 1 // async result
没有错误抛出,也就是说,这个选项并不能限制异步代码的运行时间,那应该怎么去限制全部代码的执行时间呢,目前好像没有接口终止 vm 代码的运行,若是有异步代码长时间不结束,很容易形成内存泄露,目前可行的方案是使用子进程去运行代码,若是超过限定时间尚未结果,就杀掉该子进程,另外,使用子进程还能够更方便地对内存等资源进行限制。异步
在一个全新的 V8 context 里运行代码,里面包含了语言规范规定的内置的一些函数和对象,若是咱们想要一些语言规范以外的功能或者模块,咱们须要把相应对象放到与这个 context 关联的对象里,例如在上面例子中的这句代码:
const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) { console.log(result); }};
setTimeout
不是语言规范规定的内置函数, context 自己不提供,因此咱们须要经过关联的对象传进去。
然而,当咱们把一些模块功能提供给 context 的时候,也同时带入了更多的安全隐患,请看下面来自例子:
const util = require('util'); const vm = require('vm'); const sandbox = {}; vm.createContext(sandbox); const script = new vm.Script(` // sandbox 的 constructor 是外层的 Object 类 // Object 类的 constructor 是外层的 Function 类 const OutFunction = this.constructor.constructor; // 因而, 利用外层的 Function 构造一个函数就能够获得外层的全局 this const OutThis = (OutFunction('return this;'))(); // 获得 require const require = OutThis.process.mainModule.require; // 试试 require('fs'); `,{}); const result = script.runInContext(sandbox); console.log(result === require('fs')); // true
显然,定制 context 的时候,任何一个传进去的对象或者函数均可能带来上面的问题,安全问题真的有不少工做须要作。
Github 上有一些开源的模块用于运行不信任代码,例如 sandbox,vm2,jailed等。查看这些项目的 issue 能够发现,sandbox 和 jailed 均可以用相似上面的方法突破限制,而 vm2 对这方面作了防御,其它方面也作了更多的安全工做,相对安全些。
生产中能够考虑在子进程中运行 vm2, 而后增长更低层的安全限制, 例如限制进程的权限和使用 cgroups 进行 IO,内存等资源限制,这里不详细讨论。
本文经过几个例子介绍了 Node.js 的 vm 模块以及使用 vm 模块运行不信任代码可能遇到的问题,而且对安全问题给出了一些建议。
vm
Allowing to terminate a vm context/script
V8 Embedder's Guide
ECMAScript 2015 语言规范
sandbox/issues/50
vm2/issues/32
jailed/issues/33
cgroups