JavaScript沙箱(Sandbox)是一种安全机制,用于隔离执行环境。在沙箱中运行的代码被限制在一个受控的环境中,只能访问特定的资源和执行特定的操作,而不影响宿主的原有的资源,造成对宿主运行环境的破坏。
沙箱隔离通常应用在微前端,执行第三方脚本,在线编辑器等等。
实现JavaScript沙箱有很多种方案,下面介绍一些常见的方案。
- iframe+proxy沙箱
- proxy多实例沙箱
- ShadowRealm
iframe
iframe自带js沙箱特性,能独立运行js代码。iframe提供了sandbox属性,不配置则默认开启所有可用的安全设置。
<iframe src="demo_iframe_sandbox.htm" sandbox=""></iframe>
腾讯的wujie利用iframe来执行子应用的js代码,通过iframe和web component建立连接,原理大致如下:
- 配置子应用的相关入口文件以及生命周期
- 加载子应用时解析子应用的html获取页面结构以及相关的js资源
- 把html结构插入到web component
- 通过创建iframe的window,document,location代理对象到web component,实现桥接
那么如何执行子应用的代码,把代理的window对象注入进去,这里用到了iife函数配合bind改变this上下文,绑定函数内部的this都指向代理的window, 立即执行函数的调用,传入三个相同的参数 window.proxy 。这可能是为了确保函数内部访问的全局对象是经过代理。
const scriptInstance = document.createElement('script');
const script = `(function(window, self, document, location, history) {
${scriptString}\n
}).bind(window.proxyWindow)(
window.proxyWindow,
window.proxyWindow,
window.proxyShadowDom,
window.proxyLocation,
window.proxyHistory,
);`;
scriptInstance.text = script;
document.head.appendChild(scriptInstance);
Proxy
es6提供的Proxy可以帮我们进行对象代理,微前端qiankun就是利用Proxy来代理window对象实现js沙箱隔离,支持多实例。除此之外还有window快照和基于Proxy的单例沙箱。
下面是使用Proxy实现window沙箱隔离的代码:
class ProxySandbox {
constructor() {
this.fakeWindow = Object.create(null); // 创建一个空对象作为沙箱环境
this.rawWindow = window; // 原始的全局window对象
this.proxy = new Proxy(this.fakeWindow, {
get(target, prop, receiver) {
// 如果是window对象自身的属性,则直接返回
if (prop === 'window' || prop === 'self' || prop === 'top') {
return receiver;
}
// 否则从原始window对象中获取属性
return Reflect.get(this.rawWindow, prop, this.rawWindow);
},
set(target, prop, value, receiver) {
// 允许设置属性值到沙箱环境的fakeWindow中
target[prop] = value;
// 如果属性是全局变量,则也设置到原始window对象中
if (!variableWhiteList.includes(prop)) {
this.rawWindow[prop] = value;
}
return true;
}
});
}
// 激活沙箱
activate() {
// 这里可以添加激活沙箱所需的逻辑
}
// 停用沙箱
deactivate() {
// 这里可以添加停用沙箱所需的逻辑,例如清除添加到window的全局变量
Object.keys(this.fakeWindow).forEach(key => {
if (this.fakeWindow[key] !== this.rawWindow[key]) {
this.rawWindow[key] = undefined;
}
});
}
// 获取沙箱环境
getProxy() {
return this.proxy;
}
}
// 使用示例
const sandbox = new ProxySandbox();
const isolatedWindow = sandbox.getProxy();
// 现在isolatedWindow可以作为沙箱环境使用,变量不会污染全局window
isolatedWindow.newGlobalVar = 'This will not pollute the global scope';
同样qiankun也是使用iife的方式来实现window,document和history对象的代理。
const executableScript = `;(function(window, self, globalThis){ ;${scriptText}${sourceUrl} }).bind(window.proxy)(window.proxy, window.proxy, window.proxy); `
eval.call(window, executableScript)
ShadowRealm
ShadowRealm 是一个 ECMAScript 标准提案,旨在提供一种机制,通过为不同的程序提供新的全局对象和一组 JavaScript 内置对象,来控制程序的执行。这种隔离在当前的 Web 平台中还没有实现,未来可能是微前端最好的实现方案。提案地址 https://github.com/tc39/proposal-shadowrealm
ShadowRealm API 由一个包含如下函数签名的类实现:
declare class ShadowRealm {
constructor();
evaluate(sourceText: string): PrimitiveValueOrCallable;
importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>;
}
每个 ShadowRealm 实例都有自己独立的运行环境实例,在 realm 中,提案提供了两种方法让我们来执行运行环境实例中的 JavaScript 代码:
evaluate()
:同步执行代码字符串,类似eval()
。importValue()
:返回一个 Promise 对象,异步执行代码字符串。
通过 evaluate
执行代码与 eval
类似,比如:
const sr = new ShadowRealm();
sr.evaluate(`'ab' + 'cd'`) === 'abcd' // true
但存在一些细微的差别,比如执行作用域、调用方式以及传值类型等。例如,如果 .evaluate() 返回一个函数,则该函数会被包装,这样我们就可以从外部调用它,而逻辑在 ShadowRealm 中运行,我们可以通过观察下面的 console.assert 来效果:
globalThis.realm = 'incubator realm';
const sr = new ShadowRealm();
sr.evaluate(`globalThis.realm = 'child realm'`);
const wrappedFunc = sr.evaluate(`() => globalThis.realm`);
console.assert(wrappedFunc() === 'child realm');
shadowRealm.importValue
shadowRealm.importValue
是 ShadowRealm API 中的一个方法,允许我们从一个js模块导入特性的值到 ShadowRealm 环境中,并在该隔离环境中使用该值。
shadowRealm.importValue
的函数签名如下:
shadowRealm.importValue(specifier, bindingName)
- specifier:模块的路径。跟动态
import
一样的参数。 - bindingName:从模块
export
的函数或者变量。
它允许你从一个 JavaScript 模块中导入特定的值到 ShadowRealm 的环境中。这个方法特别适用于当你需要从模块中提取一个函数或基本数据类型,并在 ShadowRealm 的隔离环境中使用这些值时。
const shadowRealm = new ShadowRealm();
// 假设我们有一个模块 './math.js',export 了一个 'add' 的函数
shadowRealm.importValue('./math.js', 'add').then(function(importedAddFunction) {
// 使用导入的函数在 ShadowRealm 中执行计算
const result = importedAddFunction(2, 3);
console.log(result); // 输出 5
});
这里仅对 ShadowRealm 提案及相关概念进行了简要介绍,虽然浏览器还不支持,不过没关系,提前熟悉下有这个特性也不是坏事,这个提案的落地可能对于一个更完美的 JavaScript 沙箱设计有所帮助。