Fork me on GitHub

管中窥豹各种沙盒技术

管中窥豹各种沙盒技术

What

沙箱,即 sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。举个简单的栗子,其实我们的浏览器,Chrome 中的每一个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(sandbox)隔离,网页 web 代码内容必须通过 IPC 通道才能与浏览器内核进程通信,通信过程会进行安全的检查。沙箱设计的目的是为了让不可信的代码运行在一定的环境中,从而限制这些代码访问隔离区之外的资源。

— 摘自:说说JS中的沙箱

一言以蔽之,沙盒(sandbox)就是程序运行时的隔离环境,以此屏蔽程序运行时对外界的一切副作用。其实我们已经在工作中接触到了各种沙盒技术:

  • docker :大名鼎鼎又耳熟能详的容器技术方案,服务端的微服务主要就是通过 docker 技术实现虚拟化的底层支持,使服务开发者可以体会不到环境的区别、抹平运行时差异的。可以说对微服务来说,docker 是这些年能得到如此的发展的一个基石。
  • 操作系统进程:程序是指令、数据及其组织形式的描述,进程是程序的实体,进程间相互隔离,通过 IPC 机制进行通信。上述 chrome tab 其实就是一个进程啦。
  • Node.js VM:Node.js 默认提供的一个内建模块,VM 模块提供了一系列 API 用于在 V8 虚拟机环境中编译和运行代码。JavaScript 代码可以被编译并立即运行,或编译、保存然后再运行。
1
2
3
4
5
const vm = require('vm');
const script = new vm.Script('m + n');
const sandbox = { m: 1, n: 2 };
const context = new vm.createContext(sandbox);
script.runInContext(context);
  • iframe:一种古老的沙盒了,方便、简单、但不怎么安全,在线代码编辑 https://codesandbox.io/s/news 就使用了 iframe 方式创建隔离环境。
1
<iframe sandbox src="..." />

Why

  • 引入第三方脚本(开源、ISV 入驻)如果不做隔离会引起安全问题,如 app crash、资损。
  • 持续🔥的“微前端”中沙盒环境保证运行时的 app 隔离和 css 隔离。

How

with + new Function + proxy

eval 和 new Function 都被用来动态执行一段脚本,两者的区别在于

  • eval 是全局对象的一个函数属性,执行的代码拥有着和应用中其它正常代码一样的的权限,它能访问「执行上下文」中的局部变量,也能访问所有「全局变量」
  • Function 构造器生成的函数,并不会在创建它的上下文中创建闭包,一般在全局作用域中被创建。当运行函数的时候,只能访问自己的本地变量和全局变量,不能访问 Function 构造器被调用生成的上下文的作用域。
1
2
3
4
function compileCode (src) {
src = 'with (sandbox) {' + src + '}'
return new Function('sandbox', src)
}

无论 eval 还是 function,执行时都会把作用域一层一层向上查找,如果找不到会一直到 global

20200730104112.jpg

那么就可以利用 ES6 特性 Proxy 的原理就是,让执行了代码在 sandobx 中找的到,以达到「防逃逸」的目的。

1
2
3
4
5
6
7
8
9
10
11
function evalute(code,sandbox) {
sandbox = sandbox || Object.create(null);
const fn = new Function('sandbox', `with(sandbox){return (${code})}`);
const proxy = new Proxy(sandbox, {
has(target, key) {
// 让动态执行的代码认为属性已存在
return true;
}
});
return fn(proxy);
}

20200730104411.jpg

上述方案在大部分场景已经够用了,但这依然不是一个绝对安全的沙盒,留个课后作业,试试看如何突破这个 sandbox 。

**
**

生产环境下的实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取异步模块
_findAsyncModule = (codePieces = []) => {
if (!isArray(codePieces) || codePieces.length <= 0) {
return null;
}

try {
new Function('REQUIRE_MAP', `;${codePieces.join('')}`)(REQUIRE_MAP);
const source = codePieces[0];
const moduleName = source.match(MODULE_NAME_IN_SOURCE_REG)[1];
return window.require(moduleName);
} catch (error) {
throw error;
}
};

阿里云微前端方案沙箱

20200730180642.jpg

  • wepback 的插件在应用代码构建的时候给子应用代码加上一层 wrap 代码,创建一个闭包,把需要隔离的浏览器原生对象变成从下面函数闭包中获取的,从而我们可以在应用加载的时候,传入模拟掉的 window,document 之类的对象。
1
2
3
4
5
6
7
// 打包代码
__CONSOLE_OS_GLOBAL_HOOK__(id, function (require, module, exports, {window, document, location, history}) {
/* 打包代码 */
})
function __CONSOLE_OS_GLOBAL_HOOK__(id, entry) {
entry(require, module, exports, {window, document, location, history})
}
  • 通过 new iframe 对象,把里面的原生浏览器对象通过 contentWindow 取出来,实现这一堆浏览器的原生对象。

阿里云开放平台微前端方案的沙箱实现:https://developer.aliyun.com/article/761446

参考