当前位置:嗨网首页>书籍在线阅读

04-沙箱和虚拟机模块

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

[toc]

3.1.2 沙箱和虚拟机模块

学习JavaScript的时候,首先要避免的就是使用eval()。原因是,eval()会使用和你的应用程序相同的上下文来执行JavaScript。意味着对于一段你一无所知或者不甚了解的代码,你要给予它们和你自己认真写好的代码一样的信任,并且执行它们。这就像你要把一段字符串直接加入到SQL语句中去执行,而不去检查里面是不是有一些不该被执行的、有问题的内容。

如果由于某种原因,你确实需要在Node应用程序中执行任意一组JavaScript,那你可以使用VM模块来将脚本装进沙箱(Sandboxing)。然而,Node开发人员提醒我们,这不是一个完全安全的方法。如果要执行任意JavaScript,唯一安全的方法是使用一个单独的进程。如果你信任代码的来源,但为了避免造成某些意料不到的结果,可以使用VM将该脚本与本地环境隔离开。

脚本可以先使用vm.Script对象进行预编译,也可以作为vm的参数直接被调用。script.runInNewContext()或vm.runInNew Context()可以在新的上下文中运行脚本,脚本无法访问局部变量或全局对象,而是会在一个有上下文的新沙箱环境中执行。这个概念可以通过下面的代码来说明。沙箱中包含两个全局变量(与Node的两个全局对象名称一样,只是重新定义了):

var vm = require('vm');
var sandbox = {
   process: 'this baby',
   require: 'that'
};
vm.runInNewContext('console.log(process);console.log(require)',sandbox);

这样运行代码会出错,因为沙箱的上下文中并不包含console对象,可以这样修改:

var vm = require('vm');
var sandbox = {
   process: 'this baby',
   require: 'that',
   console: console
};
vm.runInNewContext('console.log(process);console.log(require)',sandbox);

但是,这样就无法达到为脚本代码创建一个全新的上下文的目的了。如果希望脚本能够访问全局console对象(或其他对象),可以使用runInThisContext()函数。在例3-1中,我使用Script对象来演示如何让上下文包含全局对象,而不是本地对象。

例3-1 使一段脚本可以访问全局console

var vm = require('vm');
global.count1 = 100;
var count2 = 100;
var txt = 'if (count1 === undefined) var count1 = 0; count1++;' +
          'if (count2 === undefined) var count2 = 0; count2++;' +
          'console.log(count1); console.log(count2);';
var script = new vm.Script(txt);
script.runInThisContext({filename: 'count.vm'});
console.log(count1);
console.log(count2);

这段程序运行的结果是:

101
1
101
100

变量count1是在全局对象上声明的,而且在运行脚本的上下文中也是可用的。变量count2是局部变量,必须在上下文中重新定义。对沙箱脚本中局部变量的更改不会影响应用程序所在的上下文中同名的局部变量。

如果不在独立上下文中运行的脚本中声明变量count2,会收到一个错误。错误能被显示出来,是因为沙箱功能有一个选项是displayErrors,默认设置为true。runInThisContext()还有别的选项,比如filename,如示例代码所示;还有timeout,它是允许脚本运行的毫秒数,一旦超时,就会被终止(并引发错误)。filename选项用于指定运行脚本时在堆栈跟踪中显示的文件名。如果要为Script对象指定文件名,则一定要在创建Script对象时就指定,而不是在上下文函数调用时才指定:

var vm = require('vm');
global.count1 = 100;
var count2 = 100;
var txt = 'count1++;' +
          'count2++;' +
          'console.log(count1); console.log(count2);';
var script = new vm.Script(txt, {filename: 'count.vm'});
try {
  script.runInThisContext();
} catch(err) {
  console.log(err.stack);
}

和参数filename不同的是,Script允许另外两个全局参数(displayErrors和timeout)在上下文函数调用时指定。

运行代码会导致错误,是因为沙箱脚本无法访问应用程序中的本地变量(count2),虽然它可以访问全局变量(count1)。打印错误时,堆栈跟踪的内容被打印出来,显示的是参数filename的值。

除了直接在程序中写脚本外,我们还可以从文件中加载脚本。假如要运行下面这段脚本:

if (count1 === undefined) var count1 = 0; count1++;
if (count2 === undefined) var count2 = 0; count2++;
console.log(count1); console.log(count2);

我们可以使用这段代码来预编译并在沙箱中运行:

var vm = require('vm');
var fs = require('fs');
global.count1 = 100;
var count2 = 100;
var script = new vm.Script(fs.readFileSync('script.js','utf8'));
script.runInThisContext({filename: 'count.vm'});
console.log(count1);
console.log(count2);

为什么不能在脚本中使用文件系统模块?因为它被分配到一个局部变量中,不能在脚本中访问。为什么不能将它导入脚本?因为沙箱中没有require方法。也就是说,全局对象或函数(如require)都不能在脚本中访问。

最后一个沙箱函数是runInContext()。它也会提供一个沙箱,但沙箱必须在函数调用之前进行语境化(显式地创建上下文)。在下面的代码中,我们将直接在VM对象上调用它。请注意,我们在语境化的沙箱中添加了一个新变量,然后在应用程序中显示出来:

var vm = require('vm');
var util = require('util');
var sandbox = {
     count1 : 1
    }; 
vm.createContext(sandbox);
if (vm.isContext(sandbox)) console.log('contextualized');
vm.runInContext('count1++; counter=true;',sandbox,
                {filename: 'context.vm'});
console.log(util.inspect(sandbox));

程序运行的结果是:

contextualized
{ count1: 2, counter: true }

runInContext()函数同样支持runInThisContext()和runInNewContext()的3个选项。同样,在Script中运行脚本和直接在VM中运行的区别是Script会对代码进行预编译。另外,Script对象必须在创建时传递filename,而VM可以在函数调用时才传递参数。

**在独立进程中运行脚本** 如果你对在独立进程中运行脚本以及上下文隔离有兴趣,可以看看第三方提供的有额外保护措施的沙箱模块。下一节介绍怎样搜索这样的模块。