Sandbox(沙盒/沙箱)的主要目的是为了安全性,以防止恶意代码或者不受信任的脚本访问敏感资源或干扰其他应用程序的执行。通过在沙盒环境中运行,可以确保代码的行为被限制在一个安全的范围内,防止其超出预期权限进行操作。
沙箱(Sandbox)是一种安全机制,目的是让程序运行在一个相对独立的隔离环境,使其不对外界的程序造成影响,保障系统的安全。作为开发人员,我们经常会同沙箱环境打交道,例如,服务器中使用 Docker 创建应用容器;使用 Codesandbox运行 Demo示例;在程序中创建沙箱执行动态脚本等。
为微前端框架主要做2个工作,一个是JS的sandBox,其次是把sandbox内执行的结果 输出 webcomponts到 页面内。
通过使用沙箱,每个前端应用都可以拥有自己的上下文环境、页面路由和状态管理,而不会相互干扰或冲突。
那么如何实现JavaScript的sandbox呢?
沙盒实现分为2个类别,一个是用iframe 或ShadowRealm 在原生上实现sandbox,第二种是js特性实现sandbox(主要基于proxy)。
下面详细介绍下沙盒实现思路。
利用iframe天然隔离机制,加上postMessage通讯机制,可以快速实现一个简易沙箱。
在 iframe 中运行的脚本程序访问到的全局对象均是当前 iframe 执行上下文提供的,不会影响其父页面的主体功能,因此使用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法。
这个是腾讯的无界沙箱模式。
这个方案有一些限制:
所以需要对应的配置项来解除上述限制。
const frame = document.createElement('iframe')
// 限制沙盒
frame.sandbox = 'allow-same-origin allow-scripts'
// 当前页面给 iframe 发送消息
frame.onload = function (e) {
frame.contentWindow.postMessage(data)
}
frame.contentWindow.addEventListener('message', function (e) {
const func = new frame.contentWindow.Function('dataInIframe', code);
// 给副页面也送消息
parent.postMessage(func(e.data))
});
// 父页面接收 iframe 发送过来的消息
parent.addEventListener('message', function (e) {
console.log('parent - message from iframe:', e.data);
console.log(data.toString())
}, false);
实际工程层面的,推荐阅读:《让iframe焕发新生》,代码:https://github.com/Tencent/wujie/blob/master/packages/wujie-core/src/iframe.ts
将这套机制封装进wujie框架
于子应用完全独立的运行在iframe内,路由依赖iframe的location和history,我们还可以在一张页面上同时激活多个子应用,由于iframe和主应用处于同一个top-level browsing context,因此浏览器前进、后退都可以作用到到子应用:
这里几个核心点这里提一下:
子应用的代码 code 在 iframe 内部访问 window,document、location 都被劫持到相应的 proxy,并且还会注入 $wujie 对象供子应用调用
const script = `(function(window, self, global, document, location, $wujie) {
${code}\n
}).bind(window.__WUJIE.proxy)(
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxy,
window.__WUJIE.proxy.document,
window.__WUJIE.proxy.location,
window.__WUJIE.provide
);`;
因本问主要讨论沙箱,所以iframe 如何做到值隔离JS,DOM元素渲染到主应用,还是看无界源码。
ShadowRealm 是一个 ECMAScript 标准提案,旨在创建一个独立的全局环境,它的全局对象包含自己的内建函数与对象(未绑定到全局变量的标准对象,如 Object.prototype 的初始值),有自己独立的作用域,具体参看:https://github.com/tc39/proposal-shadowrealm
ShadowRealm允许一个JS运行时创建多个高度隔离的JS运行环境(realm),每个realm具有独立的全局对象和内建对象。
但是此方案是最佳方案,奈何还是提案阶段,所以这里做讨论了!
iframe 页面会独立一个渲染进程,所以创建一个 iframe 开销很大,如我在 Electron 项目中启动一个 iframe,可以看到 Electron 为它分配了 85 M的内存,比较恐怖。加上 WebAssembly 的内存分配,启动一个 iframe 至少会分配 100M 的内存。
WebWorker 中由于不能操作 DOM,独立的线程作为天然的沙箱环境而被其他开发者很少提及,但是看腾讯的无界方案,个人觉得用WebWorker来做沙箱还是非常不错的!
基于 IIFE 立即执行函数(自执行匿名函数)来实现。
外界不能访问函数内的变量,同时由于作用域的隔离,也不会污染全局作用域,通常用于插件和类库的开发,比如webpack打包后的代码。
//bundle.js
(() => {
var __webpack_modules__ = ({
"./src/index.js": (() => {
eval("console.log('这是个测试脚本!用于分析 webpack 打包后代码。');");
})
});
var __webpack_exports__ = {};//忽略,目前用不到它。
__webpack_modules__["./src/index.js"]();
})();
但 IIFE 只能实现一个简易的沙箱,并不算一个独立的运行环境,函数内部可以访问上下文作用域,有污染作用域的风险。
const objDemo= { a:1 };
(() => {
objDemo.a= 2;
})();
JavaScript 在查找某个未使用命名空间的变量时,会通过作用于链来查找,而 with 关键字,可以使得查找时,先从该对象的属性开始查找,若该对象没有要查找的属性,顺着上一级作用域链查找,若不存在要查到的属性,则会返回 ReferenceError 异常。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/with
不推荐使用 with,在 ECMAScript 5 严格模式中该标签已被禁止。推荐的替代方案是声明一个临时变量来承载你所需要的属性。
说明:为什么不使用eval eval() 是一个危险的函数,它使用与调用者相同的权限执行代码。如果你用 eval() 运行的字符串代码被恶意方(不怀好意的人)修改,你最终可能会在你的网页/扩展程序的权限下,在用户计算机上运行恶意代码。更重要的是,第三方代码可以看到某一个 eval() 被调用时的作用域,这也有可能导致一些不同方式的攻击。相似的 Function 就不容易被攻击。 eval() 通常比其他替代方法更慢,因为它必须调用 JS 解释器,而许多其他结构则可被现代 JS 引擎进行优化。 此外,现代 JavaScript 解释器将 JavaScript 转换为机器代码。这意味着任何变量命名的概念都会被删除。因此,任意一个 eval 的使用都会强制浏览器进行冗长的变量名称查找,以确定变量在机器代码中的位置并设置其值。另外,新内容将会通过 eval() 引进给变量,比如更改该变量的类型,因此会强制浏览器重新执行所有已经生成的机器代码以进行补偿。但是(谢天谢地)存在一个非常好的 eval 替代方法:只需使用 window.Function。这有个例子方便你了解如何将eval()的使用转变为Function()。
利用 new Function 创建的函数不需要考虑当前所在作用域,默认被创建于全局环境,因此运行时只能访问全局变量和自身的局部变量。
const ctx = {
test(flag){
console.log(flag);
}
};
function sandbox(code) {
code = "with (ctx) {" + code + "}";
return new Function("ctx", code);
}
const code = `
const name = 'zhangsan'
test(name)
`;
sandbox(code)(ctx);
利用with和Function,可以防止代码访问上下文作用域,但是对于全局对象,仍然可以访问并篡改,有污染全局的风险。
Proxy 是 ES6 提供的新语法,Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
Proxy 可以代理对象,那么我们同样可以用其代理 window——浏览器环境中的全局变量。每个 Web 应用都会与 window 交互,无数的 API 也同样挂靠在 window 上,要实现全局环境的安全访问,首先需要 window 隔开。 主要实现思路是基于 get、set、has、getOwnPropertyDescriptor 等关键拦截器对 window 进行代理拦截(如下如有涉及代码,我们主要关注 get 与 set 两类拦截器)
沙箱保证了内部程序执行的安全运行,但是极端情况下仍然有些人试图摆脱这种束缚,入侵内部程序,这种行为被称为沙箱逃逸。
With 再加上 Proxy 几乎完美解决 JS 沙箱机制。但是如果对象的Symbol.unScopables设置为 true ,会无视 with 的作用域直接向上查找,造成沙箱逃逸,所以要另外处理 Symbol.unScopables。
具体代码实现(核心思路是通过 with 块和 Proxy 对象来隔离执行环境,确保执行的代码只能访问到沙盒内的变量。任何在沙盒内声明或者修改的变量都不会影响到全局作用域,同时,全局作用域下的变量在沙盒内也是不可见的)
// 创建一个沙盒对象,这个对象里面的属性和全局作用域不同步,避免沙盒内代码影响外部环境
const sandboxProxy = new Proxy({}, {
has: function() {
// 拦截属性检查,总是返回 false,迫使 with 块中的查找进入沙盒对象
return true;
},
get: function(target, key) {
if (key === Symbol.unscopables) return undefined;
// 返回沙盒对象中的属性,如果不存在则返回 undefined
return target[key];
},
set: function(target, key, value) {
// 设置属性值,影响只限于沙盒内部
target[key] = value;
return true;
}
});
// 定义执行沙盒代码的函数
function executeSandboxCode(code) {
/*
// 通过 new Function 创建一个新的函数,这样保证了函数体内的代码运行在全局作用域之外
const sandboxFunction = new Function('sandbox', `with(sandbox) { ${code} }`);
// 调用这个新创建的函数,传入沙盒代理对象
sandboxFunction(sandboxProxy);
*/
// 避免绕过沙盒,通过改变 this 指向的代码示例
const sandboxFunction = new Function('sandbox', 'with(sandbox) { return function() { "use strict"; ' + code + ' } }');
sandboxFunction(sandboxProxy).call(null);
}
使用上,
executeSandboxCode(`
// 这些代码运行在沙盒环境中,外部变量对其不可见
var secret = '我是沙盒中的秘密';
console.log(secret); // 输出: '我是沙盒中的秘密'
`);
上面的沙盒实现是很简单的,并不严格,存在多种方式可以绕过这个沙盒的限制来访问或影响全局作用域。
// 创建一个更安全的沙盒环境
function createSandboxEnvironment() {
const sandbox = Object.create(null); // 创建一个没有原型的对象
// 重新定义全局构造函数,禁止在沙盒中使用它们创建新的全局变量
const Function = () => {
throw new Error('Function constructor is disabled in the sandbox');
};
// 可以继续禁用或重写沙盒中的其它功能
// ...
// 设置一个安全的代理,以防沙盒代码尝试逃逸
const sandboxProxy = new Proxy(sandbox, {
has: () => true,
get: (target, key) => {
if (key === Symbol.unscopables) return undefined;
return target[key];
},
set: (target, key, value) => {
target[key] = value;
return true;
}
});
// 返回代理沙盒和用于执行代码的函数
return {
run: (code) => {
const sandboxFunction = new Function('sandbox', `with(sandbox) { return function() { 'use strict'; ${code} } }`);
return sandboxFunction(sandboxProxy).call(sandbox);
}
};
}
// 使用更安全的沙盒环境
const sandboxEnv = createSandboxEnvironment();
// 测试沙盒环境的安全性
sandboxEnv.run(`//测试代码`);
此沙盒代码虽然对一些安全隐患做出了改进,但它依然无法保证绝对安全。尤其是对于有意图绕过沙盒限制的代码,
我们主要基于阿里的乾坤来说明
单实例只针对全局运行环境进行代理赋值记录,而不从中取值,那么这样的沙箱只是作为我们记录变化的一种手段,而实际操作仍在主应用运行环境中对 window 进行了读写,因此这类沙箱也只能支持单实例模式,qiankun 在实现上将其命名为 LegacySandbox,可以看其源码:
https://github.com/umijs/qiankun/blob/master/src/sandbox/legacy/sandbox.ts
rebindTarget2Fn实现可参考:https://github.com/umijs/qiankun/blob/master/src/sandbox/common.ts
在单实例的场景总,通过fakeWindow一个空的对象,其没有任何储存变量的功能,如果在微应用创建的变量最终实际都是挂载在window上的,这就限制了同一时刻不能有两个激活的微应用。 这方面推荐看一下乾坤的源码:https://github.com/umijs/qiankun/blob/master/src/sandbox/proxySandbox.ts
其主要做的目的是:
沙箱在初始构造时建立一个状态池,当应用操作 window 时,赋值通过 set 拦截器将变量写入状态池,而取值也是从状态池中优先寻找对应属性。由于状态池与子应用绑定,那么运行多个子应用,便可以产生多个相互独立的沙箱环境。
我们把他的代码简化下:
class SandBox {
constructor() {
this.proxy = null;
this.fakeWindow = {};
this.active = false;
}
// 激活沙箱
activate() {
if (this.active) {
return;
}
this.active = true;
// 创建一个代理来管理 window 对象
this.proxy = new Proxy(window, {
// 取值操作时,优先从 fakeWindow 状态池取值
get: (target, property) => {
return Object.prototype.hasOwnProperty.call(this.fakeWindow, property)? this.fakeWindow[property] : target[property];
},
// 设置操作时,写入 fakeWindow 状态池
set: (target, property, value) => {
if (this.active) {
this.fakeWindow[property] = value;
return true;
}
target[property] = value;
return true;
},
// 其他拦截器...
});
}
// 关闭沙箱
deactivate() {
if (!this.active) {
return;
}
this.active = false;
}
}
在这个简化的例子中,SandBox 类被用来创建和管理沙箱。每个沙箱实例在构造时创建了一个 fakeWindow 的状态池,用来存储对 window 的本地更改,而不影响真正的全局 window 对象。
activate 方法通过对 window 对象创建一个 Proxy 来激活沙箱。当沙箱活跃时,读操作(get)会优先从 fakeWindow 中获取属性值,所有写操作(set)只会影响 fakeWindow,而不影响全局 window 对象。
每个微前端应用在启动时会得到它自己的沙箱实例,因此它们会有自己的状态池和拦截逻辑,这允许应用独立地操作全局对象而不互相干扰。
由于 Proxy 为 ES6 引入的 API,在不支持 ES6 的环境下,我们可以通过一类原始的方式来实现所要的沙箱,即利用普通对象针对 window 属性值构建快照,用于环境的存储与恢复,并在应用卸载时对 window 对象修改做 diff 用于子应用环境的更新保存。在 qiankun 中也有该降级方案,被称为 SnapshotSandbox。当然,这类沙箱同样也不能支持多实例运行,原因也相同。
这类方案的主要思路与 LegacySandbox 有些类似,同样主要分为激活与卸载两个部分的操作。
具体可以参看:https://github.com/umijs/qiankun/blob/master/src/sandbox/snapshotSandbox.ts
由于未使用到 Proxy,且只利用 Object 的操作来实现,这个沙箱机制是三类机制中最简单的一种。
多实例运行 | 语法兼容 | 不污染全局环境(主应用) | |
---|---|---|---|
LegacySanbox | ❌ | ❌ | ❌ |
ProxySandbox | ✅ | ❌ | ✅ |
SnapshotSandbox | ❌ | ✅ | ❌ |
iframe | ✅ | ✅ | ✅ |
转载本站文章《微前端学习笔记(3):前端沙箱之JavaScript的sandbox(沙盒/沙箱)》, 请注明出处:https://www.zhoulujun.cn/html/webfront/engineer/Architecture/9055.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
扫码关注腾讯云开发者
领取腾讯云代金券
Copyright © 2013 - 2025 Tencent Cloud. All Rights Reserved. 腾讯云 版权所有
深圳市腾讯计算机系统有限公司 ICP备案/许可证号:粤B2-20090059 深公网安备号 44030502008569
腾讯云计算(北京)有限责任公司 京ICP证150476号 | 京ICP备11018762号 | 京公网安备号11010802020287
Copyright © 2013 - 2025 Tencent Cloud.
All Rights Reserved. 腾讯云 版权所有