写点什么

漫谈 B 端的沙箱技术

  • 2010-06-02
  • 本文字数:3207 字

    阅读完需:约 11 分钟

从语言学的角度上来说,允许代码无节制地使用全局变量,是最错误的选择之一。而更可怕的,就是一个变量"可能"成为全局的(在未知的时间与地点)。但是这两项,却伴随 JavaScript 这门语言成功地走到了现在。

也许是限于浏览器应用的规模,所以这一切还迟迟没有酿成灾难。在此之前,出现了两种解决方案。一种是 ECMA 在新的规范(Edition 5)中对此做出了限制,其中最重要的一条便是 eval() 的使用变得不再随意和无度。而另一种方案,则是相对没有那么官僚与学术的,尽管也拥有一个同样学术的名字:沙箱。

沙箱(Sandbox)并不是一个新东西,即使对于 JavaScript 来说,也已经存在了相当长的时间。在 SpiderMonkey JS 的源代码中,就明确地将一个闭包描述为一个沙箱。这包含着许多潜在的信息:它有一个初始环境,可以被重置,可以被复制,以及最重要的,在它内部的所有操作,不会影响到外部

当然事实上远非如此。JavaScript 里的闭包只是一个"貌似沙箱"的东西–仍然是出于 JavaScript 早期的语言规范的问题,闭包不得不允许那些"合法泄漏"给外部的东西。而对于这一切无法忍受的前端工程师们,开始寻求另外的解决之道,这其中相对较早的尝试,是基于 IFRAME 的实践。例如 dean.edwards 在 2006 年提出过的方案(注 1):

复制代码
a_frames.document.write(
"<script>"+
"var MSIE/*@cc_on =1@*/;"+ // sniff
"parent.sandbox=MSIE?this:{eval:function(s){return eval(s)}}"+
"<\/script>"
);

显然,由于在不同的 IFRAME 中运行着各自的 JavaScript 引擎实例,所以上述的方案也意味着沙箱是"引擎"这个级别的:在任何一个沙箱中的崩溃,将导致该引擎以及对应 IFRAME 崩溃。但–理论上说–不会影响整个浏览器。

问题是,这并不那么理想。往往的,引擎会导致整个浏览器锁在那里,例如用 alert() 弹出一个对话框而又因为某种意外失去了焦点。又或者单个的 IFRAME 会导致全局的 CPU 被耗光,例如一个死循环。于是更加复杂的方案–在 JavaScritp 中包含一个完整的执行器–出现了。最有名的则是 Narrative JavaScript,它内建了一个执行器,用于逐行地解释执行 JavaScript 代码,这使得它可以控制所有的代码执行序列,或者随时重置整个执行引擎–如同一个沙箱所要做的那样。

这一切或者太过依赖于环境,又或者太过复杂,但都不乏追随者。例如 jsFiddle 这个项目(注 2)在"嵌入或装载"这样的路子上就已经有了不俗的成绩。但是,YUI 在新版本中却给出了它自己的选择:以更加明确的编程约定,来实现应用级别的沙箱。这包括一个非常简单的、新的 YUI 语法:

复制代码
YUI().use('dom-base', function(Y) {
// Y 是一个新的沙箱
});

在’dom-base’位置上,可以是 1 到 N 个字符串,表明一个需要在沙箱中装载的模块列表。这可以是沙箱的初始列表,或者后续的 callback 函数 (亦即是用户代码) 所需依赖的模块列表。在这种实现方案中,YUI 为每个沙箱维护各自的装载模块列表和上下文环境中的变量、成员。但是出于 JavaScript 语言自己的局限,这个沙箱依然是相当脆弱的。例如下一示例中沙箱内的代码就会污染到全局:

复制代码
YUI().use('', function(Y) {
abc = 1234; //<-- 这里可能导致一个全局变量'abc'被隐式地声明
});

同样,在上述的沙箱里也可以使用类似 window、document 等全局变量、修改它们的成员或无限制地调用方法(例如使用 setTimeout() 来创建时钟)。所以 YUI 的沙箱事实上是靠"规约"来约束的,而不是真正意义上的沙箱。当然,这也意味着,如果用户能按照规约来处理沙箱内的代码,那么也就能自由地享用它带来的便利:安全、移植和有效的隔离副作用。

而我们再穷究其根底,YUI 沙箱的实质不过是一行:

复制代码
// code from yui.js
// - mod.fn(this, name)
mod.entryFunc(sandbox, modName);

其实际含义是:

  • mod :沙箱当前装载的模块;
  • entryFunc : 上述模块的入口函数;
  • sandbox :当前的沙箱的实例,即 YUI() 返回值;
  • modName:模块名

除了依赖关系(以及可能需要的异步加载)之外,YUI 沙箱环境仅是用下面的代码来简单地调用 callback 函数:

复制代码
callback(Y, response);

然而这些需求的实现并不那么复杂。首先,我们设定数据结构 mod 为一个对象:

复制代码
{ name:modName, fn: entryFunc, req: [], use: [] }

则一个环境对象 env,将包括多个 mod(将它们处理成对象而非数组,主要是便于使用名字来索引模块) 和以及对它们进行管理操作的方法:

复制代码
{ mods:{}, used:{}, add:..., use:...}

最后,所谓一个沙箱 sandbox,就是上述环境对象的一个实例,并在初始时 sandbox.mods 与 sandbox.used 为空。由此简单的实现为:

复制代码
/**
* tiny sandbox framework
* mirror from YUI3 by aimingoo.
**/
function Sandbox() {
if (!(this instanceof arguments.callee)) return new arguments.callee();
this.mods = this.mods || {};
this.used = {};
}
Sandbox.prototype = {
add: function(modName, entryFunc, reqArr, useArr) {
this.mods[modName] = { fn: entryFunc, req: reqArr, use: useArr }
},
use: function() {
var mods = [].slice.call(arguments, 0); // 0..length-2 is modNames
var callback = mods.pop(); // length-1 is callback
var recursive_load = function(name, mod) {
if (!this.used[name] && (mod=this.mods[name])) {
mod.req.forEach(recursive_load, this);
mod.fn(this, name);
mod.use.forEach(recursive_load, this);
this.used[name] = true;
}
}
mods.forEach(recursive_load, this);
callback(this);
}
}

现在我们来尝试一个与 YUI 类似的语法风格:

复制代码
Sandbox().use('', function(){
alert('user code.');
});

或者,先向整个 Sandbox 环境注册一些模块(在真实的框架实现中,这一步可能是通过框架的装载器来初始化):

复制代码
// for test, entry of mods
f1 = function() { alert('f1') };
f2 = function() { alert('f2') };
f3 = function() { alert('f3') };
// mods for global/common env.
Sandbox.prototype.mods = {
'core': { fn: f1, req: [], use: [] },
'oo': { fn: f2, req: ['core'], use: ['xml'] },
'xml': { fn: f3, req: [], use: [] }
}

然后再尝试在一个沙箱实例中运行代码:

复制代码
// f1 -> f2 -> f3 -> user code
Sandbox().use('oo', function(){
alert('user code.');
});

其实即便是上述代码中用于处理模块依赖的逻辑,也并不是什么"神奇的"代码或者技巧。除开这些,这样的沙箱隔离泄露的能力还抵不过一个嵌入式 DSL 语言。而后者所应用的技巧很简单,看不出什么花招(注 3):

复制代码
with (YUI()) this.eval("... mod_context ... ");

这样一来,在 mod_context 里的代码就只会在 YUI() 的一个实例中造成污染了。当然,仍然是源于 JavaScript 的限制,我们还是无法避免一个变量泄露到全局–除非,我们回到 js in js 这个项目(注 4),真的在环境中重新初始化一个 js 引擎。

从这一意义上来说,引擎级别的沙箱与操作系统的进程一样,带来的是终级的解决方案,所以 Chrome、IE 等等主流浏览器纷纷有了"独立进程"模式。而在这样的背景之下,试图用"框架内置沙箱"来改善 ECMAScript ed3 中一些设计疏失的种种努力,不过是一张张空头的支票罢了。

甚至,用这本支票签完单也未必有人会收的。

备注

注 1: http://dean.edwards.name/weblog/2006/11/sandbox/
注 2: http://jsfiddle.net/
注 3: http://blog.csdn.net/aimingoo/archive/2009/09/08/4532496.aspx
注 4: http://mxr.mozilla.org/mozilla/source/js/narcissus/


作者简介:周爱民,国内软件开发界资深软件工程师,架构师。有十余年的软件开发、项目管理、团队建设的经验,历任部门经理、区域总经理、高级软件工程师、平台架构师等职。现任支付宝(中国)公司业务架构师。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

2010-06-02 00:057411

评论

发布
暂无评论
发现更多内容

ARMS 用户体验监控正式发布原生鸿蒙应用 SDK

阿里巴巴云原生

阿里云 云原生 可观测

利用item_get API:深入探索淘宝商品详情的获取

代码忍者

淘宝API接口 tb商品详情数据接口

这本秘籍送到心趴上了

CodeBuddy

智能排产能解 决工厂什么问题?

万界星空科技

mes 万界星空科技mes 自动排产 智能排产

EndNote 21大客户授权版 Mac(最强文献管理软件) 永久激活版

Rose

融云斩获两项行业大奖,技术团队和出海服务接连获赞

融云 RongCloud

主机管理软件WGCLOUD介绍 - 部署在公网运行时配置建议增强安全性

王逅逅

服务器安全 zabbix Prometheus #运维 Linux 运维

AI Agent与MEME:技术与文化融合驱动Web3创新

TechubNews

国云官网焕新升级,共创数智未来!

天翼云开发者社区

云计算 天翼云

天津市级人工智能通识课超10w人学习,和鲸Heywhale人工智能通识课解决方案重磅上线!

ModelWhale

Python 人工智能 大数据

生产管理用上自动排产,企业离智慧工厂又进了一步

万界星空科技

mes 万界星空科技 自动排产 生产计划 智能排产

区块链 RWA 系统开发的技术难点

北京木奇移动技术有限公司

区块链技术 软件外包公司 RWA开发

Permute 3:一键转换,媒体文件轻松驾驭

Rose

Microsoft Office 2019 免激活最新版-mac/win

Rose

微信授权全链路打通指南

不在线第一只蜗牛

微信

Magnet for Mac中文版 苹果电脑窗口管理软件

Rose

Spring中的循环依赖是怎么个事?

伤感汤姆布利柏

Axure RP 11 for mac中文破解版 及安装教程

Rose

iStat Menus 6中文 for Mac 最强大的macOS系统监控软件

Rose

Redis Desktop Manager for Mac(Redis桌面管理工具) 中文激活版

Rose

专为Mac电脑用户设计的虚拟定位工具 AnyGo for Mac中文破解版

Rose

大数据与AI:从分析到预测的跃迁

天津汇柏科技有限公司

大数据‘’ AI 人工智能

智谱AI:引领国内开源大模型的创新与商业化

测试人

人工智能 软件测试

漫谈B端的沙箱技术_Java_周爱民_InfoQ精选文章