浅谈Node.js 沙箱环境


Posted in Javascript onMay 15, 2018

node官方文档里提到node的vm模块可以用来做沙箱环境执行代码,对代码的上下文环境做隔离。

\A common use case is to run the code in a sandboxed environment. The sandboxed code uses a different V8 Context, meaning that it has a different global object than the rest of the code.

先看一个例子

const vm = require('vm');
let a = 1;
var result = vm.runInNewContext('var b = 2; a = 3; a + b;', {a});
console.log(result);  // 5
console.log(a);     // 1
console.log(typeof b); // undefined

沙箱环境中执行的代码对于外部代码没有产生任何影响,无论是新声明的变量b,还是重新赋值的变量a。 注意最后一行的代码默认会被加上return关键字,因此无需手动添加,一旦添加的话不会静默忽略,而是执行报错。

const vm = require('vm');
let a = 1;
var result = vm.runInNewContext('var b = 2; a = 3; return a + b;', {a});
console.log(result);
console.log(a);
console.log(typeof b);

如下所示

evalmachine.<anonymous>:1
var b = 2; a = 3; return a + b;
         ^^^^^^

SyntaxError: Illegal return statement
  at new Script (vm.js:74:7)
  at createScript (vm.js:246:10)
  at Object.runInNewContext (vm.js:291:10)
  at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:17)
  at Module._compile (internal/modules/cjs/loader.js:678:30)
  at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
  at Module.load (internal/modules/cjs/loader.js:589:32)
  at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
  at Function.Module._load (internal/modules/cjs/loader.js:520:3)
  at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)

除了runInNewContext外,vm还提供了runInThisContext和runInContext两个方法都可以用来执行代码 runInThisContext无法指定context

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInThisContext('localVar += "vm";');
console.log('vmResult:', vmResult);
console.log('localVar:', localVar);
console.log(global.localVar);

由于无法访问本地的作用域,只能访问到当前的global对象,因此上面的代码会因为找不到localVal而报错

evalmachine.<anonymous>:1
localVar += "vm";
^

ReferenceError: localVar is not defined
  at evalmachine.<anonymous>:1:1
  at Script.runInThisContext (vm.js:91:20)
  at Object.runInThisContext (vm.js:298:38)
  at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:21)
  at Module._compile (internal/modules/cjs/loader.js:678:30)
  at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
  at Module.load (internal/modules/cjs/loader.js:589:32)
  at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
  at Function.Module._load (internal/modules/cjs/loader.js:520:3)
  at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)

如果我们把要执行的代码改成直接赋值的话就可以正常运行了,但是也产生了全局污染(全局的localVar变量)

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInThisContext('localVar = "vm";');
console.log('vmResult:', vmResult);  // vm
console.log('localVar:', localVar);  // initial value
console.log(global.localVar);     // vm

runInContext在传入context参数上与runInNewContext有所区别 runInContext传入的context对象不为空而且必须是经vm.createContext()处理过的,否则会报错。 runInNewContext的context参数是非必须的,而且无需经过vm.createContext处理。 runInNewContext和runInContext因为有指定context,所以不会向runInThisContext那样产生全局污染(不会产生全局的localVar变量)

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInNewContext('localVar = "vm";');
console.log('vmResult:', vmResult);  // vm
console.log('localVar:', localVar);  // initial value
console.log(global.localVar);     // undefined

当需要一个沙箱环境执行多个脚本片段的时候,可以通过多次调用runInContext方法但是传入同一个vm.createContext()返回值实现。

超时控制及错误捕获

vm针对要执行的代码提供了超时机制,通过指定timeout参数即可以runInThisContext为例

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInThisContext('while(true) { 1 }; localVar = "vm";', { timeout: 1000});
vm.js:91
   return super.runInThisContext(...args);
          ^

Error: Script execution timed out.
  at Script.runInThisContext (vm.js:91:20)
  at Object.runInThisContext (vm.js:298:38)
  at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:21)
  at Module._compile (internal/modules/cjs/loader.js:678:30)
  at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
  at Module.load (internal/modules/cjs/loader.js:589:32)
  at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
  at Function.Module._load (internal/modules/cjs/loader.js:520:3)
  at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)
  at startup (internal/bootstrap/node.js:228:19)

可以通过try catch来捕获代码错误

const vm = require('vm');
let localVar = 'initial value';​
try { 
  const vmResult = vm.runInThisContext('while(true) { 1 }; localVar = "vm";', {
    timeout: 1000
  });
} catch(e) { 
  console.error('executed code timeout');
}

延迟执行

vm除了即时执行代码之外,也可以先编译然后过一段时间再执行,这就需要提到vm.Script了。其实无论是runInNewContext、runInThisContext还是runInThisContext,背后其实都创建了Script,从之前的报错信息就可以看出来 接下来我们就用vm.Script来重写本文开头的例子

const vm = require('vm');
let a = 1;
var script = new vm.Script('var b = 2; a = 3; a + b;');
setTimeout(() => { 
  let result = script.runInNewContext({a}); 
  console.log(result);   // 5 
  console.log(a);     // 1 
  console.log(typeof b);  // undefined
}, 300);

除了vm.Script,node在9.6版本中新增了vm.Module也可以做到延迟执行,vm.Module主要用来支持ES6 module,而且它的context在创建的时候就已经绑定好了,关于vm.Module目前还需要在命令行使用flag来启用支持

node --experimental-vm-module index.js

vm作为沙箱环境安全吗?

vm相对于eval来说更安全一些,因为它隔离了当前的上下文环境了,但是尽管如此依然可以访问标准的JS API和全局的NodeJS环境,因此vm并不安全,这个在官方文档里就提到了

The vm module is not a security mechanism. Do not use it to run untrusted code

请看下面的例子

const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()")
console.log("The app goes on...") // 永远不会输出

为了避免上面这种情况,可以将上下文简化成只包含基本类型,如下所示

let ctx = Object.create(null);
ctx.a = 1; // ctx上不能包含引用类型的属性
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);

针对原生vm存在的这个问题,有人开发了vm2包,可以避免上述问题,但是也不能说vm2就一定是安全的

const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")().exit()');

虽然执行上述代码没有问题,但是由于vm2的timeout对于异步代码不起作用,所以下面的代码永远不会执行结束。

const { VM } = require('vm2');
const vm = new VM({ timeout: 1000, sandbox: {}});
vm.run('new Promise(()=>{})');

即使希望通过重新定义Promise的方式来禁用Promise的话,还是一个可以绕过的

const { VM } = require('vm2');
const vm = new VM({ 
 timeout: 1000, sandbox: { Promise: function(){}}
});
vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
JavaScript将数组转换成CSV格式的方法
Mar 19 Javascript
javascript实现点击提交按钮后显示loading的方法
Jul 03 Javascript
javascript中的previousSibling和nextSibling的正确用法
Sep 16 Javascript
将JavaScript的jQuery库中表单转化为JSON对象的方法
Nov 17 Javascript
js实现对ajax请求面向对象的封装
Jan 08 Javascript
详解Node.js项目APM监控之New Relic
May 12 Javascript
用React实现一个完整的TodoList的示例代码
Oct 30 Javascript
Vue 项目分环境打包的方法示例
Aug 03 Javascript
解决layer弹出层msg的文字不显示的问题
Sep 11 Javascript
layui 地区三级联动 form select 渲染的实例
Sep 27 Javascript
为什么node.js不适合大型项目
Apr 28 Javascript
JavaScript 数组去重详解
Sep 15 Javascript
npm全局模块卸载及默认安装目录修改方法
May 15 #Javascript
WebPack配置vue多页面的技巧
May 15 #Javascript
修改npm全局安装模式的路径方法
May 15 #Javascript
修改node.js默认的npm安装目录实例
May 15 #Javascript
Vue中的scoped实现原理及穿透方法
May 15 #Javascript
vue-cli项目中使用Mockjs详解
May 14 #Javascript
vue使用自定义icon图标的方法
May 14 #Javascript
You might like
PHP中对各种加密算法、Hash算法的速度测试对比代码
2014/07/08 PHP
CodeIgniter表单验证方法实例详解
2016/03/03 PHP
php 微信公众平台开发模式实现多客服的实例代码
2016/11/07 PHP
PHP实现关键字搜索后描红功能示例
2019/07/03 PHP
jQuery 1.0.2
2006/10/11 Javascript
How to Auto Include a Javascript File
2007/02/02 Javascript
javascript各种复制代码收集
2008/09/20 Javascript
Prototype源码浅析 Enumerable部分(二)
2012/01/18 Javascript
基于jquery的DIV随滚动条滚动而滚动的代码
2012/07/20 Javascript
js获取dom的高度和宽度(可见区域及部分等等)
2013/06/13 Javascript
Node.js的MongoDB驱动Mongoose基本使用教程
2016/03/01 Javascript
node.js实现爬虫教程
2020/08/25 Javascript
关于AngularJs数据的本地存储详解
2017/01/20 Javascript
js浏览器滚动条卷去的高度scrolltop(实例讲解)
2017/07/07 Javascript
详解Vue2 添加对scss的支持
2019/01/02 Javascript
解决vue项目中页面调用数据 在数据加载完毕之前出现undefined问题
2019/11/14 Javascript
javascript数组元素删除方法delete和splice解析
2019/12/09 Javascript
vscode 插件开发 + vue的操作方法
2020/06/05 Javascript
完美解决通过IP地址访问VUE项目的问题
2020/07/18 Javascript
[02:14]2016国际邀请赛中国区预选赛Ehome晋级之路
2016/07/01 DOTA
python中对list去重的多种方法
2014/09/18 Python
零基础写python爬虫之打包生成exe文件
2014/11/06 Python
python删除过期文件的方法
2015/05/29 Python
Django中对数据查询结果进行排序的方法
2015/07/17 Python
利用Python+Java调用Shell脚本时的死锁陷阱详解
2018/01/24 Python
python中利用zfill方法自动给数字前面补0
2018/04/10 Python
梅尔倒谱系数(MFCC)实现
2019/06/19 Python
Python3.8.2安装包及安装教程图文详解(附安装包)
2020/11/28 Python
泰国最新活动和优惠:Megatix
2020/05/07 全球购物
办公室主任职责范文
2013/11/08 职场文书
读书演讲主持词
2014/03/18 职场文书
关爱留守儿童标语
2014/06/18 职场文书
公安民警正风肃纪剖析材料
2014/10/10 职场文书
2015重阳节敬老活动总结
2015/07/29 职场文书
2015重阳节座谈会主持词
2015/07/30 职场文书
小学四年级作文之写景
2019/08/23 职场文书