/ JavaScript

不完美沙箱 - 劫持全局对象

上一章中对需求进行了分析,并对实现方案有了一定的探讨,最终得出从劫持全局对象这一环节入手的结论,而本文将讨论如何劫持这个全局对象。

全局对象,多数会进行Javascript编程的工程师都知道,在浏览器的执行环境中,叫做window,虽然事实上window和全局对象并不严格意义上是一个东西,不过这对我们的实现并不会造成什么障碍。

剖析全局对象

既然要去做劫持全局对象这样不人道的事,自然先要知道全局对象是个什么东西,有什么样的特点,以便针对目标,一刀致命。这个时候,标准就会成为我们最好的参考,首先打开ECMAScript 5的HTML版本,此后几乎所有的标准引用都会从这里产生。

所谓擒贼先擒王,经典理论告诉我们,不可以把整个洋洋洒洒数万字的标准给全看了,必须以快、狠、准的手段找到我们需要的东西。如果是纸质的书籍,对于一个不熟悉文档结构的人而言自然是无处下手,但好在现在是数字化的时代,我们有一个名为“搜索”的强大功能。

在浏览器中按下CTRL+F的组合键,随后输入Global,看看一共有多少内容。从目录来看,一共有7处内容,确实不多:

  1. 10.2.3 The Global Environment
  2. 10.4. Entering Global Code
  3. 15.1 The Global Object
  4. 15.1.1 Value Properties of the Global Object
  5. 15.1.2 Function Properties of the Global Object
  6. 15.1.4 Constructor Properties of the Global Object
  7. 15.1.5 Other Properties of the Global Object

而这8处内容又可以分为2部分,其中1-3分别介绍了全局对象本身的特点,而4-7则专注于介绍全局对象上的内置属性(即本地对象)的相关内容。仅仅以劫持全局对象为目的的我们,自然可以先不管全局对象上的其它属性,只针对全局对象自身入手,因此将目光放置在前3者之上。

首先是来自于The Global Environment一节对全局对象概念上的介绍:

The global environment is a unique Lexical Environment which is created before any ECMAScript code is executed. The global environment’s Environment Record is an object environment record whose binding object is the global object (15.1). The global environment’s outer environment reference is null.

As ECMAScript code is executed, additional properties may be added to the global object and the initial properties may be modified.

这一段并没有带来太多的信息,仅仅说明了“全局对象是什么”这一理论,但是其中的第二段却给我们一点有利地信息:在代码执行过程中,全局对象上可能会增加额外的属性,当前的属性也可能被修改。这就告诉我们,需要模拟全局对象的时候,可以在上面添加自己的内容,也可以让原有的原生对象变化甚至消失,这些都不会违反标准,因此在全局对象上添加诸如container或者user之类的业务相关信息也合情合理,而去除eval之类的函数也合乎逻辑,使得API的设计更加容易。

随后,将重点放在Entering Global Code一章中:

The following steps are performed when control enters the execution context for global code:

  1. Initialize the execution context using the global code as described in 10.4.1.1.
  2. Perform Declaration Binding Instantiation as described in 10.5 using the global code.

这一段没有什么有效的信息,不过告诉我们10.4.1.1节会有相关的内容(当然10.5节也有内容,但这一节并不专为全局环境服务,因此不再赘述,有兴趣的可以自行翻阅):

The following steps are performed to initialize a global execution context for ECMAScript code C:

  1. Set the VariableEnvironment to the Global Environment.
  2. Set the LexicalEnvironment to the Global Environment.
  3. Set the ThisBinding to the global object.

这一段的信息就非常重要了,他告诉我们2个事实:

  • 全局环境下的VariableEnvironment是全局对象本身,即所有通过var声明的变量都会成为全局对象的属性
  • 全局环境下的ThisBinding是全局对象本身,即this对象为全局对象。

这也是全局对象最大的特征,那通过var声明和通过this.x声明会有几乎相同的效果

最后再看看15.1节带给我们的信息(摘录部分):

Unless otherwise specified, the standard built-in properties of the global object have attributes {[[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true}.

The global object does not have a [[Construct]] internal property; it is not possible to use the global object as a constructor with the new operator.

The global object does not have a [[Call]] internal property; it is not possible to invoke the global object as a function.

The values of the [[Prototype]] and [[Class]] internal properties of the global object are implementation-dependent.

这一段告诉我们以下事实:

  • 所有全局对象上的内置属性都是可写不可遍历不可删除的。
  • 全局对象本身不是一个构造器,且不能被调用。亦即全局对象是个普通对象,并非函数。
  • 全局对象的[[Prototype]][[Class]]是什么与标准没关系,因此模拟的时候不需要考虑这个环节。

到这里,相关的标准也基本翻阅完毕,从标准中我们可以总结,如果需要模拟出一个全局对象,则需要满足以下条件:

  1. 开发者提交的代码中使用var定义的变量和this.xwindow.x定义的属性均会出现在这个对象上。
  2. 全局对象上需要一些本地对象,如ObjectArray等,且这些对象可以被修改,但无法通过for..in遍历出来,且不可被delete运算符删除。

测试先行

本着满足上节中提到的2个要求的目的,为了可以验证最终实现的沙箱的可用性,我们必须有一些测试的代码,来检验成果。

根据2个要求,在此先设计2个测试用例:

// 测试var, this, window三者是否相通
function testBinding() {
    var x = 3;
    equals(this.x, 3);
    equals(window.x, 3);

    this.y = 4;
    equals(y, 4);
    equals(window.y, 4);

    window.z = 5;
    equals(z, 5);
    equals(this.z, 5);
}

// 测试是否存在内置对象,且不可被遍历及删除
function testNative() {
    var natives = ['Object', 'String', 'Number', 'RegExp', 'Array', 'Date'];
    var keys = {};
    for (var name in window) {
        keys[name] = true;
    }

    for (var i = 0; i < natives.length; i++) {
        var key = natives[i];
        if (keys[key]) {
            fail();
        }
        var value = window[key];
        delete window[key];
        if (window[key] !== value) {
            fail();
        }
    }
}

至此,开工的先决条件已经具备,而未来的2个篇章,将着重从这2个测试用例下手,以一章一个问题的速度给予解决。而解决过程中自然少不了对标准的查阅、引用,毕竟这一系列的目的就是展示标准如何引导问题的解决

在此之前,如果有兴趣的,可以尝试着来制作一个沙箱,沙箱的目标是在用户提交的脚本前后插入一些代码,让用户的代码只能在我们模拟的全局对象中运行,不可越级访问到真实的全局对象。只要符合以上2点的沙箱就是好沙箱哦~

PS:一开始从脑子里挖标准的时候,觉得对全局对象的形容也就是最后2句话而已,所以本想这一章可以将第一个测试用例搞定。但没想真正从标准是引用内容的时候,竟然有如此大的篇幅,导致本章还没有空余来真正着手解决问题,这也从另一方面展现了标准的严谨性和权威性。由于下面要处理的变量及对象绑定问题也会有大量的标准引用来支撑,因此本系列会多出一个章节,单独来处理这个问题。