浅析 Node.js 的 vm 模块以及运行不信任代码
在一些系统中,我们希望给用户提供插入自定义逻辑的能力,除了 RPC 和 REST 之外,运行客户提供的代码也是比较常用的方法,好处是可以极大地减少在网络上的耗时。JavaScript 是一种非常流行而且容易上手的语言,因此,让用户用 JavaScript 来写自定义逻辑是一个不错的选择。下面我们介绍 Node.js 提供的 vm 模块以及分析用它来运行不信任代码可能遇到的问题。
vm 模块
vm 模块是 Node.js 内置的核心模块,它能让我们编译 JavaScript 代码和在指定的环境中运行。请看下面例子:
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 ===www.bsck.org 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 是一个类,用于创建代码实例,后面可以多次运行。
vm.createContext(sandbox) 用于 "contextify" 一个对象,根据 ECMAScript 2015 语言规范,代码的执行需要一个 execution context。这里的 "contextify",就是把传进去的对象与 V8 的一个新的 context 进行关联。这里所说的关联,我的理解是,这个 "contextified" 对象的属性将会成为那个 context 的全局属性,同时,在 context 下运行代码时产生的全局属性也会成为这个 "contextified" 对象的属性。
script.runInContext(contextifiedSandbox) 就是使代码在 contextifiedSandbox 这个 context 中运行,从上面的输出可以看到,代码运行后,contextifiedSandbox 里面的属性的值已经被改变了,运行结果是最后一个表达式的值。
除了上面几个接口之外,vm 模块还有一些更便捷的接口,例如 vm.runInContext(code, contextifiedSandbox[,www.90168.org options]),vm.runInNewContext(code[, sandbox][, options])等,详细可看文档。
外层如何得到代码运行结果
我们用 vm 运行代码的时候很可能需要得到一些结果,从上面的例子中可以看到,我们可以通过把结果作为最后一个表达式的值传给外层,或者作为context 的属性给外层使用,这在同步代码里没有问题,但是假如结果需要依赖里面的异步操作呢?这时,我们可以通过在 context 里放一个回调函数。 下面是例子:
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 代码的运行,如果有异步代码长时间不结束,很容易造成内存泄露,目前可行的方案是使用子进程去运行代码,如果超过新视觉限定时间还没有结果,就杀掉该子进程,另外,使用子进程还可以更方便地对内存等资源进行限制。
定制 context 与安全问题
在一个全新的 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 模块运行不信任代码可能遇到的问题,并且对安全问题给出了一些建议。
浅析 Node.js 的 vm 模块以及运行不信任代码的更多相关文章
- Node.js入门:模块机制
CommonJS规范 早在Netscape诞生不久后,JavaScript就一直在探索本地编程的路,Rhino是其代表产物.无奈那时服务端JavaScript走的路均是参考众多服务器端语言来 ...
- 浅析Node.js的Event Loop
目录 浅析Node.js的Event Loop 引出问题 Node.js的基本架构 Libuv Event Loop Event Loop Phases Overview Poll Phase The ...
- node.js的File模块
1.Node.js是什么? (1) Nodejs是为了开发高性能的服务器而诞生的一种技术 (2) 简单的说 Node.js 就是运行在服务端的 JavaScript,基于V8进行运行 (3) Node ...
- Web 前端模块出现的原因,以及 Node.js 中的模块
模块出现原因 简单概述 随着 Web 2.0 时代的到来,JavaScript 不再是以前的小脚本程序了,它在前端担任了更多的职责,也逐渐地被广泛运用在了更加复杂的应用开发的级别上. 但是 JavaS ...
- 利用Node.js的Net模块实现一个命令行多人聊天室
1.net模块基本API 要使用Node.js的net模块实现一个命令行聊天室,就必须先了解NET模块的API使用.NET模块API分为两大类:Server和Socket类.工厂方法. Server类 ...
- Node.js的Formidable模块的使用
今天总结了下Node.js的Formidable模块的使用,下面做一些简要的说明. 1) 创建Formidable.IncomingForm对象 var form = new formidab ...
- Node.js的net模块
net模块提供了一个异步网络包装器,用于TCP网络编程,它包含了创建服务器和客户端的方法 创建TCP服务器 net.createServer方法 创建客户端去连接服务器 net.connect方法 简 ...
- node.js中express模块创建服务器和http模块客户端发请求
首先下载express模块,命令行输入 npm install express 1.node.js中express模块创建服务端 在js代码同文件位置新建一个文件夹(www_root),里面存放网页文 ...
- node.js中ws模块创建服务端和客户端,网页WebSocket客户端
首先下载websocket模块,命令行输入 npm install ws 1.node.js中ws模块创建服务端 // 加载node上websocket模块 ws; var ws = require( ...
随机推荐
- POJ Dollar Dayz 美元假日(完全背包,常规+大数)
题意:给出整数n和k,n代表拥有的钱数量,k代表有k种工具,其价钱分别为1~k.求n元能有多少种购买的方案. 思路:k最大有100,数量过大,要用大数.其他的基本和完全背包一样. #include & ...
- 如何给SAP Cloud Connector Region列表中添加新的Region
SAP help里提供了CloudFoundry和Neo环境下可用的Region和API endpoint: 当我们期望用SAP Cloud Connector连接某个SAP云平台Region时,一般 ...
- code Gym 100500D T-shirts(暴力)
因为只能买一次,暴力枚举一下买的衣服的大小. #include<cstdio> #include<map> #include<algorithm> using na ...
- BCB:UTF8Encode、AnsiToUtf8
UTF8Encode: Call Utf8Encode to convert a Unicode string to UTF-8. WS is the Unicode string to conver ...
- Bootstrap 页面标题(Page Header)
Bootstrap页面标题(PageHeader)是个不错功能,它会网页的标题的四周添加适当的间距,当一个网页中有多个标题并且每个标题之间需要添加一定适当的间距,使用页面标题是非常有用的.如果需要使用 ...
- Objective-C实现一个简单的栈
栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表.它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据(最后一个数据被第一个读出 ...
- 【转】关于“using namespace std”
对于一个存在着标准输入输出的C++控制台程序,一般会在#include <iostream>的下一行发现一句话,using namespace std.这句话其实就表示了所有的标准库函数都 ...
- PCA检测人脸的简单示例_matlab实现
PCA检测人脸的简单示例,matlab R2009b上实现训练:训练用的20副人脸: %训练%Lx=X'*Xclear;clc;train_path='..\Data\TrainingSet\';ph ...
- MySQL 自学笔记_Union(组合查询)
1. Union查询简介 组合查询:有时在使用select语句进行数据查询时,想要将多个select语句在一个查询结果中输出,此时就需要使用Union关键字. Union的使用方法:用union将多个 ...
- 03大端和小端(Big endian and Little endian)
1.大端和小端的问题 对于整型.长整型等数据类型,Big endian 认为第一个字节是最高位字节(按照从低地址到高地址的顺序存放数据的高位字节到低位字节),而 Little endian 则相反 ...