不完美沙箱 - 目的、背景及分析

目的

这将是一个系列的文章,大致由4篇博文组成,内容应该会是这样的:

  1. 目的、背景及分析
  2. 劫持全局对象
  3. 劫持全局对象属性
  4. 存在的问题及漏洞

起草这一系列博文的原因有两点,第一自然是因为最近突然有这样的需求,而群里正好也在某个深夜(技术宅基本深夜活动)讨论到了一个类似的话题,给了我一些灵感,所以决定将近期“研究”的成果展示一下。

而第二个目的,也是我想将这么一个简单的东西(其中最终就10几行代码)用整整5个博文的篇幅来说的原因,是因为近期有不少朋友向我强烈地表达了标准的存在意义极其微弱,我能把代码写好就足够的想法,而且现在更是催生了一种叫做jQuery工程师的职位,大有星星之火的样子。

对于这样的态度,作为一个只会理论不懂实践的2B工程师,个人自然是坚持地抱以反击态度的,因此,这一系列的博文,将会从“构建一个沙箱”这样的场景出发,将对于这一话题的研究过程展现于众,期间将频繁涉及到对标准的参阅,以展现标准如何引导问题的解决这一事实。

在阅读这一系列的博文前,还请确认几件事:

  • 你懂英文,因为我不是翻译专家,没能力也没篇幅将标准相关的内容都翻译一下。对于标准,我会将相关的原文引用于此,但肯定是英文的。
  • 你得抱着标准无用论来看待问题,如果你也觉得标准很好用,那还是看最后的成品代码吧,因为其中的过程于你已经失去了大部分的意义。

最后,还是先声明一件事,其实我研究这个问题并没有这么麻烦,因为本系列博文中引用标准的内容几乎全在我的脑子里。我只是将这个思考、查找、得出结论的过程放大、放慢,进行分解,一点一点地通过文字表达出来而已。至于类似“根本不知道标准又怎么会往这方面想”的问题如果出现在你的大脑中,那么恭喜你似乎已经意识到了标准学习的些许价值。

背景

如果你有一个小小的项目,想提供一个开放地平台,供第三方的开发者提交一些小程序(Widget)并嵌入到页面中使用。这有点类似与人人网

对于此类的系统,一个比较合理的设计,自然是让开发者提交一个HTML片段,并配以相应的CSS和Javascript,随后在页面中提供一个容器,引入该HTML片段,并且通过一定的手段引入CSS和Javascript,并保证CSS和Javascript只影响到该容器内的所有DOM元素

于是,这样的设计就引入了一个问题。正如上文所说,我们必须保证开发者提供的CSS及Javascript仅作用在容器及其子元素之上。对于CSS,可以使用style元素的scoped属性,虽然这个属性的定义一直处于讨论之中,但这并不妨碍它可以基本满足我们的需求。但是对于Javascript,却并没有这么简单。事实上,至今为止并没有任何的HTML属性或者Javascript函数,可以让一段Javascript在指定的作用域内执行,达到不污染全局不影响外围DOM元素等目标。

基于此,我们必须人为地创造出一个环境,在这个环境中所执行的代码要求能满足以下几点:

  • 无法在全局对象下创建任何属性。
  • 所有选择DOM的方法返回的NodeList仅包含容器及其子元素。
  • 不得使用一些高危险性的函数,如evaldocument.write
  • 尽可能对开发者透明,即开发者提交正常执行环境下可用的Javascript片段,则在系统中也同样可用。

根据专业术语,我们将这个人为创造的环境,称为沙箱(sandbox),以下是Wikipedia上对sandbox一词的解释:

In computer security, a sandbox is a security mechanism for separating running programs. It is often used to execute untested code, or untrusted programs from unverified third-parties, suppliers, untrusted users and untrusted websites.

在追求Javascript的安全执行上,很多团队都有着丰富的积累和成果,其中以Facebook的FBML微软的Web SandboxGoogle的caja最为典型,百度泛用户体验上也有一篇文章对几个较为成熟的技术方案进行了介绍。

但是这几个方案,对于一个小小的项目来说,无疑都太过沉重,不是引入一个新的语法,就是带上后端的虚拟机,同时又需要约束开发者的提交的代码,使得其对第三方小程序开发者无法透明,更加大了系统的推广成本。因此,我们试图以防君子不防小人为目标,去创造一个轻量级的沙箱环境,在保证Javascript代码基本受限地执行,又无需引入太多外部的环境依赖。

分析

重新回顾一下我们对沙箱的要求:

  • 保护全局环境不受污染。
  • DOM的获取在可控范围内。
  • 提供有限的API。
  • 对开发者透明。

反过来想,正常的开发者是怎么写代码的呢?先随便来一段:

var time = new Date;  
document.getElementById('time').innerHTML = formatDate(time, 'yyyy-MM-dd HH:mm:ss');  
document.getElementById('hello').innerHTML = 'Hello ' + username;  

对于一个认为自己是在纯净的环境中开发的开发者而言,使用document.getElementById来获取DOM元素,使用var来定义变量,随后对DOM进行操作,是再正常不过的事。但是在一个拥有容器的环境中,以上代码却适时地暴露出了几个问题:

  • var定义的变量会污染全局。
  • document.getElementById会获取到不应该能够获取的DOM元素。

此两者是一个正常的开发者(非存心搞破坏的那类)最容易在不经意之间遭遇的问题,这不是他们蓄意的破坏,只是他们认为自己的Javascript片段的执行环境是纯净、隔离、独立的。因此抱着防君子之心,对此做出限制是合理且必要的。

纵观这些问题,经过一段时间的思考(思考过程过于繁琐就不说了),可以发现,其实问题集中在一点上,即拦截全局对象。由于在Javascript中,本地对象和宿主对象以及DOM对象都是和全局对象挂钩的,且全局对象是唯一的入口,因此只要将入口劫持,所有的东西都可以得到合理地控制。如以上的问题,如果能够对全局对象进行劫持,随后提供刻意“伪造”的document对象,则问题迎刃而解。

因此,从下一节开始,将从全局对象的概念开始,讲述如何支持全局对象。并在此之后,讲述如何在劫持后伪造出相关的本地和宿主对象。