我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。

本文作者:空山

什么是沙箱

沙箱即 SandBox,它是一种安全机制,用于严格控制访问资源。通过在程序中创建一个独立的运行环境,把一些来源不可信、具有破坏力或者又是无法判定的恶意程序使其在该环境下运行,隔离了对外部程序的影响,这样即使发生了错误或者安全问题都不会影响到外面。

我们根据实现的方案不同, SandBox可以分为两种模式:

  • 单实例模式:全局只存在一个实例,直接代理原生的window对象,记录每个沙箱内window对象上的增删改等操作,激活某个沙箱时恢复上一次失活时的状态,失活时恢复原来window的状态。
  • 多实例模式:代理一个全新的window对象,所有的更改基于这个全新的对象,多个实例之间互不影响。

沙箱的应用场景

基于上面对沙箱的介绍,简而言之我们最终的目的还是为了保障程序的正常运行,通过隔离的手段避免错误、异常或者恶意代码的影响,在我们日常开发或者接触中,也有很多这样的场景,以下列举几个:

  • 微前端:微前端场景下,各个子应用被集成到一个运行时,避免每个子应用互相影响,导致的一些如全局污染的问题,下面会以QianKun为例进行详细的讲述
  • JSONP:当运行通过 <script>标签的url返回的JS代码时,为了规避一定程度上的风险可能需要在沙箱中执行
  • 在线编辑器:在某些场景下我们会提供一个编辑器或者类似的可输入界面需要用户自主的编辑代码,然后去执行它,比如:CodeSandBox对于用户输入的不确定代码为了防止污染最好是在沙箱中执行

我们把它们进行抽象的归类,大概可以分为以下三类:

  • 执行时:执行不确定、不可信的JS代码
  • 引入时:为引入的JS代码提供隔离环境
  • 访问时:执行代码对全局对象的访问和修改进行限制

JS沙箱的常见解决方案

在实现JS沙箱问题之前我们需要有两点需要注意:

  • 构建独立的上下文环境
  • 模拟浏览器的原生对象

基于这两点,目前给出以下几种方案:

with

with语句将改变作用域,会让内部的访问优先从传入的对象上查找。怎么理解呢,我们来看一下这一段代码:

const obj = {
a: 1
}
const obj2 = {
b: 2
}
const a = 9 const fun = (obj) => {
with(obj) { // 相当于{}内访问的变量都会从obj上查找
console.log(a)
a = 3
}
} fun(obj) // 1
console.log(obj) // { a: 3 }

在当前的内部环境中找不到某个变量时,会沿着作用作用域链一层层向上查找,如果找不到就拋出ReferenceError异常。我们看下下面这个例子:

const obj = {
a: 1
}
const obj2 = {
b: 2
}
const b = 9 const fun = (obj) => {
with(obj) {
console.log(a, b)
}
} fun(obj) // 1 9
fun(obj2) // ReferenceError: a is not defined

虽然with实现了在当前上下文中查找变量的效果,但是仍然存在一下问题:

  • 找不到时会沿着作用域链往上查找
  • 当修改存在的变量时,会同步修改外层的变量

除此之外with还有其他的一些弊端详细了解

ES6 proxy

为了解决with存在的问题,我们来了解下proxy方法。proxy用于创建一个对象的代理,从而实现对基本操作的拦截以及自定义。

基本语法

/**
* @param {*} target - 使用Proxy包装的目标对象
* @param {*} handler - 通常为一个函数,函数的各个属性分别定义了执行各个操作时的代理行为
*/
const p = new Proxy(target, handler)

详细了解

改进流程

code实现

const obj = {
a: 1
}
const obj2 = {
b: 2
}
let b = 3 // 用with改变作用域
const withedCode = (code) => {
// return (obj) => {
// with(ctxProxy(obj)){
// eval(code)
// }
// }
code = `with(obj) { ${ code } }`
const fun = new Function('obj', code)
return fun
} // 执行代码
const code = 'console.log(b)' // 白名单
const whiteList = ['console', 'b'] // // 访问拦截
const ctxProxy = (ctx) => new Proxy(ctx, {
has: (target, prop) => { // 当返回false的时候会沿着作用域向上查找,true为在当前作用域进行查找
if(whiteList.includes(prop)) {
return false
}
if(!target.hasOwnProperty(prop)) {
throw new Error(`can not find - ${prop}!`)
}
return true
},
}) withedCode(code)(ctxProxy(obj2)) // 3

思考:为啥需要把console添加到whiteList中?

Tips:该案例在浏览器中运行正常,在node中运行可能出现问题,原因是使用了new Function,创建的函数只能在全局作用域中运行,而在node中顶级的作用域不是全局作用域,当前全局声明的变量是在当前模块的作用域里的。详细查看:Function

这样一个简单的沙箱是不是完成了,那我们现在会不会想这样一个问题?

解决完对象的访问控制,我们现在解决第二个问题如何模拟浏览器的全局对象———iframe

还有人不清楚为啥需要模拟浏览器的对象吗?

  • 一些全局对象方法的使用比如上面的console.log
  • 获取全局对象中的一些初始变量

with + proxy + iframe

我们把原生浏览器的对象取出来

const iframe = document.createElement('iframe')
document.body.append(iframe)
const globalObj = iframe.contentWindow

创建一个全局代理对象的类

class GlobalProxy{
constructor(shareState) {
return new Proxy(globalObj, {
has: (target, prop) => {
if(shareState.includes(prop)) {
return false
}
if(!target.hasOwnProperty(prop)) {
throw new Error(`can not find - ${prop}!`)
}
return true
}
})
}
}

实际效果:

// 创建共享白名单
const shareState = [] // 创建一个沙箱实例
const sandBox = new GlobalProxy(shareState) const withedCode = (code) => {
code = `with(obj) { ${ code } }`
const fun = new Function('obj', code)
return fun
} sandBox.abc = 123 // 执行代码
const code = 'console.log(abc)' withedCode(code)(sandBox)
console.log(abc) //------console------
// 123
// undefined

Web Workers

通过创建一个独立的浏览器线程来达到隔离的目的,但是具有一定的局限性

  • 不能直接操作DOM节点
  • 不能使用windowwindow对象的默认方法和属性

......

原因在于workers运行在另一个全局上下文中,不同于当前的window。

因此适用的场景大概类似于一些表达式的计算等。

通信方式

workers和主线程之间通过消息机制进行数据传递

  • postMessage——发送消息
  • onmessage——处理消息

我们来简单看个例子:

// index.js
window.app = '我是元数据'
const myWorker = new Worker('worker.js') myWorker.onmessage = (oEvent) => {
console.log('Worker said : ' + oEvent.data)
}
myWorker.postMessage('我是主线程!') // worker.js
postMessage('我是子线程!');
onmessage = function (oEvent) {
postMessage("Hi " + oEvent.data);
console.log('window', window)
console.log('DOM', document)
} // -------------console-------------
// Worker said : 我是子线程!
// Worker said : Hi 我是主线程!
// Uncaught ReferenceError: window is not defined

详细了解

沙箱逃逸

沙箱逃逸即通过各种手段摆脱沙箱的束缚,访问沙箱外的全局变量甚至是篡改它们,实现一个沙箱还需要预防这些情况的发生。

Symbol.unscopables

Symbol.unscopables设置了true会对with进行无视,沿着作用域进行向上查找。

举个例子:

const obj = {
a: 1
} let a = 10 obj[Symbol.unscopables] = {
a: true
} with(obj) {
console.log(a) // 10
}

改进上述with + proxy + iframe中的全局代理对象的类

class GlobalProxy{
constructor(shareState) {
return new Proxy(globalObj, {
has: (target, prop) => {
if(shareState.includes(prop)) {
return false
}
if(!target.hasOwnProperty(prop)) {
throw new Error(`can not find - ${prop}!`)
}
return true
},
get: (target, prop) => {
// 处理Symbol.unscopables逃逸
if(prop === Symbol.unscopables) return undefined
return target[prop]
}
})
}
}

window.parent

可以在沙箱的执行上下文中通过该方法拿到外层的全局对象

const iframe = document.createElement('iframe')
document.body.append(iframe)
const globalObj = iframe.contentWindow class GlobalProxy{
constructor(shareState) {
return new Proxy(globalObj, {
has: (target, prop) => {
if(shareState.includes(prop)) {
return false
}
if(!target.hasOwnProperty(prop)) {
throw new Error(`can not find - ${prop}!`)
}
return true
},
get: (target, prop) => {
// 处理Symbol.unscopables逃逸
if(prop === Symbol.unscopables) return undefined return target[prop]
}
})
}
} // 创建共享白名单
const shareState = [] // 创建一个沙箱实例
const sandBox = new GlobalProxy(shareState) const withedCode = (code) => {
code = `with(obj) { ${ code } }`
const fun = new Function('obj', code)
return fun
} sandBox.abc = 123
sandBox.aaa = 789 sandBox[Symbol.unscopables] = {
aaa: true
} var aaa = 123 // 执行代码
const code = 'console.log(parent.test = 789)' withedCode(code)(sandBox)
console.log(window.test) // 789

改进方案

get: (target, prop) => {
// 处理Symbol.unscopables逃逸
if(prop === Symbol.unscopables) return undefined
// 阻止window.parent逃逸
if(prop === 'parent') {
return target
}
return target[prop]
}

原型链逃逸

通过某个变量的原型链向上查找,从而达到篡改全局对象的目的

const code = `([]).constructor.prototype.toString = () => {
return 'Escape!'
}` console.log([1,2,3].toString()) // Escape!

……

未来可尝试的新方案

ShadowRealms

它是未来JS的一项功能,目前已经进入stage-3。通过它我们可以创建一个单独的全局上下文环境来执行JS。

关于ShadowRealms的更多详情:

Portals

类似于iframe的新标签。

关于portals的更多详情

探究QianKun中的沙箱

有了上面的知识储备以后,让我们来看看QianKun中的沙箱是怎么样子的,以下只讲述一些关键代码,源码地址:https://github.com/umijs/qiankun/tree/master/src/sandbox,版本为v2.6.3

我们进入index文件看下

// 是否支持Proxy代理
if (window.Proxy) {
sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
} else {
sandbox = new SnapshotSandbox(appName);
}

我们可以看到QianKun里的沙箱主要分为三种

  • LegacySandbox:单实例代理沙箱,简单来讲就是只存在一个window实例,所有的操作都是对这一个实例的操作
  • ProxySandbox:多实例代理沙箱,通过对window的拷贝建立多个副本,在沙箱中对建立的副本进行操作
  • SnapshotSandbox:快照沙箱,基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器

SnapshotSandbox

我们先来看下SnapshotSandbox这个沙箱,源码:

// 遍历对象
function iter(obj: typeof window, callbackFn: (prop: any) => void) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop) || prop === 'clearInterval') {
callbackFn(prop);
}
}
} /**
* 基于 diff 方式实现的沙箱,用于不支持 Proxy 的低版本浏览器
*/
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy; name: string; type: SandBoxType; sandboxRunning = true; private windowSnapshot!: Window; private modifyPropsMap: Record<any, any> = {}; constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
} active() {
// 记录当前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
}); // 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
}); this.sandboxRunning = true;
} inactive() {
this.modifyPropsMap = {}; iter(window, (prop) => {
if (window[prop] !== this.windowSnapshot[prop]) {
// 记录变更,恢复环境
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
}); if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
} this.sandboxRunning = false;
}
}

结合流程图



分块解读一下,这里我们把它分成两块来看

激活时

active() {
this.windowSnapshot = {} as Window;
iter(window, (prop) => { // 通过遍历的方式记录当前window的状态,即window的当前快照
this.windowSnapshot[prop] = window[prop];
}); Object.keys(this.modifyPropsMap).forEach((p: any) => { //
window[p] = this.modifyPropsMap[p]; // 通过遍历恢复上一次失活时的变更
}); this.sandboxRunning = true;
}

失活时

inactive() {
this.modifyPropsMap = {}; iter(window, (prop) => { // 遍历window上的属性
if (window[prop] !== this.windowSnapshot[prop]) {
this.modifyPropsMap[prop] = window[prop]; // 记录和快照不一致的属性到修改的对象中
window[prop] = this.windowSnapshot[prop]; // 恢复window的属性为初始的属性
}
}); this.sandboxRunning = false;
}

SnapshotSandbox比较简单,由于不支持代理,所有的更改都在window上,只是在激活沙箱的时候保存一个window的初始快照,并在期间对变更的属性进行记录,失活时恢复初始的window,但是会造成全局window的污染。

LegacySandbox

我们来看下LegacySandbox沙箱,该沙箱基于Proxy实现的

流程图



源码部分

// 判断对象上的某个属性描述是否是可更改或者是可删除的
function isPropConfigurable(target: WindowProxy, prop: PropertyKey) {
const descriptor = Object.getOwnPropertyDescriptor(target, prop);
return descriptor ? descriptor.configurable : true;
} /**
* 基于 Proxy 实现的沙箱
* TODO: 为了兼容性 singular 模式下依旧使用该沙箱,等新沙箱稳定之后再切换
*/
export default class LegacySandbox implements SandBox {
/** 沙箱期间新增的全局变量 */
private addedPropsMapInSandbox = new Map<PropertyKey, any>(); /** 沙箱期间更新的全局变量 */
private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>(); /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
private currentUpdatedPropsValueMap = new Map<PropertyKey, any>(); name: string; proxy: WindowProxy; globalContext: typeof window; type: SandBoxType; sandboxRunning = true; latestSetProp: PropertyKey | null = null; // 设置globalContext对象上的属性
private setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) {
if (value === undefined && toDelete) {
delete (this.globalContext as any)[prop];
} else if (isPropConfigurable(this.globalContext, prop) && typeof prop !== 'symbol') {
Object.defineProperty(this.globalContext, prop, { writable: true, configurable: true });
(this.globalContext as any)[prop] = value;
}
} active() {
if (!this.sandboxRunning) {
this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
} this.sandboxRunning = true;
} inactive() {
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true)); this.sandboxRunning = false;
} constructor(name: string, globalContext = window) {
this.name = name;
this.globalContext = globalContext;
this.type = SandBoxType.LegacyProxy;
const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this; const rawWindow = globalContext;
const fakeWindow = Object.create(null) as Window; const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window = true) => {
if (this.sandboxRunning) {
if (!rawWindow.hasOwnProperty(p)) {
addedPropsMapInSandbox.set(p, value);
} else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
// 如果当前 window 对象存在该属性,且 record map 中未记录过,则记录该属性初始值
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
} currentUpdatedPropsValueMap.set(p, value); if (sync2Window) {
// 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
(rawWindow as any)[p] = value;
} this.latestSetProp = p; return true;
} // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
}; const proxy = new Proxy(fakeWindow, {
set: (_: Window, p: PropertyKey, value: any): boolean => {
const originalValue = (rawWindow as any)[p];
return setTrap(p, value, originalValue, true);
}, get(_: Window, p: PropertyKey): any {
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// or use window.top to check if an iframe context
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy;
} const value = (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
}, // trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(_: Window, p: string | number | symbol): boolean {
return p in rawWindow;
}, getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
}, defineProperty(_: Window, p: string | symbol, attributes: PropertyDescriptor): boolean {
const originalValue = (rawWindow as any)[p];
const done = Reflect.defineProperty(rawWindow, p, attributes);
const value = (rawWindow as any)[p];
setTrap(p, value, originalValue, false); return done;
},
}); this.proxy = proxy;
}
}

同样的我们来分块解读一下

这里有三个主要的变量,我们需要先知道下

/** 沙箱期间新增的全局变量 */
private addedPropsMapInSandbox = new Map<PropertyKey, any>(); /** 沙箱期间更新的全局变量,记录的是激活子应用时window上的初始值 */
private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>(); /** 持续记录更新的(新增和修改的)全局变量的 map,用于在任意时刻做 snapshot */
private currentUpdatedPropsValueMap = new Map<PropertyKey, any>(); // 白名单,是微应用之间全局共享的变量
const variableWhiteList: PropertyKey[] = [
'System',
'__cjsWrapper',
...variableWhiteListInDev,
]

激活时

active() {
if (!this.sandboxRunning) {
// 把上一次沙箱激活时的变更,设置到window上
this.currentUpdatedPropsValueMap.forEach((v, p) => this.setWindowProp(p, v));
}
this.sandboxRunning = true;
}

失活时

inactive() {
...
// 通过遍历还原window上的初始值
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => this.setWindowProp(p, v));
// 通过对新增属的遍历,去除window上新增的属性
this.addedPropsMapInSandbox.forEach((_, p) => this.setWindowProp(p, undefined, true));
this.sandboxRunning = false;
}

Proxy代理

const proxy = new Proxy(fakeWindow, {
set: (_: Window, p: PropertyKey, value: any): boolean => {
const originalValue = (rawWindow as any)[p];
// 把变更的属性同步到addedPropsMapInSandbox、modifiedPropsOriginalValueMapInSandbox以及currentUpdatedPropsValueMap
return setTrap(p, value, originalValue, true);
}, get(_: Window, p: PropertyKey): any {
// 防止通过使用top、parent、window、self访问外层真实的环境
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy;
} const value = (rawWindow as any)[p];
//一些异常处理获取value的真实值,主要处理了window.console、window.atob这类API在微应用中调用时会抛出 Illegal invocation异常的问题
return getTargetValue(rawWindow, value);
}, has(_: Window, p: string | number | symbol): boolean {
// 访问的属性是否在rawWindow上,不在返回false沿着作用域向上查找
return p in rawWindow;
}, // 拦截对象的Object.getOwnPropertyDescriptor()操作
getOwnPropertyDescriptor(_: Window, p: PropertyKey): PropertyDescriptor | undefined {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
}, // 拦截对象的Object.defineProperty()操作
defineProperty(_: Window, p: string | symbol, attributes: PropertyDescriptor): boolean {
const originalValue = (rawWindow as any)[p];
const done = Reflect.defineProperty(rawWindow, p, attributes);
const value = (rawWindow as any)[p];
setTrap(p, value, originalValue, false); // 变更属性记录 return done;
},
}); const setTrap = (p: PropertyKey, value: any, originalValue: any, sync2Window = true) => {
if (this.sandboxRunning) {
// 判断是否为rawWindow自己的属性
if (!rawWindow.hasOwnProperty(p)) {
// 新增的属性存入addedPropsMapInSandbox
addedPropsMapInSandbox.set(p, value);
} else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) {
// 修改的属性把初始值存入modifiedPropsOriginalValueMapInSandbox中
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
} // 当前数据的所有变更记录在currentUpdatedPropsValueMap中
currentUpdatedPropsValueMap.set(p, value); if (sync2Window) {
// 必须重新设置 window 对象保证下次 get 时能拿到已更新的数据
(rawWindow as any)[p] = value;
} this.latestSetProp = p; return true;
} // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
}

仍然是操作window对象,会造成全局window的污染,但是不需要记录window的初始快照,也不需要对window进行自身属性的整个遍历相比于diff快照效率会高点,性能会好点。

ProxySandbox

接下来我们来看下ProxySandbox这个沙箱,该沙箱通过创建一个window的副本fakeWindow实现每个ProxySandbox实例之间属性互不影响

流程图



首先我们需要创建一个window副本fakeWindowpropertiesWithGetter,后一个是用来记录有getter且不可配置的Map对象,具体实现参考proxy中的get部分

function createFakeWindow(globalContext: Window) {
// 记录 window 对象上的 getter 属性,原生的有:window、document、location、top
const propertiesWithGetter = new Map<PropertyKey, boolean>();
const fakeWindow = {} as FakeWindow; Object.getOwnPropertyNames(globalContext)
// 遍历出window上所有不可配置的属性
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
return !descriptor?.configurable;
})
.forEach((p) => {
// 获取属性描述符
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
if (
p === 'top' ||
p === 'parent' ||
p === 'self' ||
p === 'window' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
descriptor.configurable = true; if (!hasGetter) {
descriptor.writable = true;
}
} if (hasGetter) propertiesWithGetter.set(p, true);
// 冻结某个属性,冻结以后该属性不可修改
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor));
}
}); return {
fakeWindow,
propertiesWithGetter,
};
}

ProxySandbox沙箱源码:

export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>(); name: string; type: SandBoxType; proxy: WindowProxy; globalContext: typeof window; sandboxRunning = true; latestSetProp: PropertyKey | null = null; private registerRunningApp(name: string, proxy: Window) {
if (this.sandboxRunning) {
const currentRunningApp = getCurrentRunningApp();
if (!currentRunningApp || currentRunningApp.name !== name) {
setCurrentRunningApp({ name, window: proxy });
}
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTask(() => {
setCurrentRunningApp(null);
});
}
} active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
} inactive() {
if (--activeSandboxCount === 0) {
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
// @ts-ignore
delete this.globalContext[p];
}
});
} this.sandboxRunning = false;
} constructor(name: string, globalContext = window) {
this.name = name;
this.globalContext = globalContext;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this; const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext); const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || globalContext.hasOwnProperty(key); const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
this.registerRunningApp(name, proxy);
// We must kept its description while the property existed in globalContext before
if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// @ts-ignore
target[p] = value;
} if (variableWhiteList.indexOf(p) !== -1) {
// @ts-ignore
globalContext[p] = value;
} updatedValueSet.add(p); this.latestSetProp = p; return true;
} // 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
}, get: (target: FakeWindow, p: PropertyKey): any => {
this.registerRunningApp(name, proxy); if (p === Symbol.unscopables) return unscopables;
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') {
return proxy;
} // hijack globalWindow accessing with globalThis keyword
if (p === 'globalThis') {
return proxy;
} if (
p === 'top' ||
p === 'parent' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
// if your master app in an iframe context, allow these props escape the sandbox
if (globalContext === globalContext.parent) {
return proxy;
}
return (globalContext as any)[p];
} // proxy.hasOwnProperty would invoke getter firstly, then its value represented as globalContext.hasOwnProperty
if (p === 'hasOwnProperty') {
return hasOwnProperty;
} if (p === 'document') {
return document;
} if (p === 'eval') {
return eval;
} const value = propertiesWithGetter.has(p)
? (globalContext as any)[p]
: p in target
? (target as any)[p]
: (globalContext as any)[p];
/* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
See this code:
const proxy = new Proxy(window, {});
const proxyFetch = fetch.bind(proxy);
proxyFetch('https://qiankun.com');
*/
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
return getTargetValue(boundTarget, value);
}, // trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(target: FakeWindow, p: string | number | symbol): boolean {
return p in unscopables || p in target || p in globalContext;
}, getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
/*
as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
> A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
*/
if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p);
descriptorTargetMap.set(p, 'target');
return descriptor;
} if (globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
descriptorTargetMap.set(p, 'globalContext');
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
} return undefined;
}, // trap to support iterator with sandbox
ownKeys(target: FakeWindow): ArrayLike<string | symbol> {
return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target)));
}, defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
const from = descriptorTargetMap.get(p);
/*
Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
otherwise it would cause a TypeError with illegal invocation.
*/
switch (from) {
case 'globalContext':
return Reflect.defineProperty(globalContext, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
}, deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {
this.registerRunningApp(name, proxy);
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p); return true;
} return true;
}, // makes sure `window instanceof Window` returns truthy in micro app
getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},
}); this.proxy = proxy; activeSandboxCount++;
}
}

同样我们把它分成几块来理解

激活时

active() {
// 记录激活沙箱的数量
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}

失活时

inactive() {
...
if (--activeSandboxCount === 0) {
// variableWhiteList记录了白名单属性,需要在沙箱全部失活时进行属性的删除
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
delete this.globalContext[p];
}
});
} this.sandboxRunning = false;
}

Proxy代理

set部分

set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
// 记录当前运行的微应用
this.registerRunningApp(name, proxy);
// 当前target不存在,但是globalContext中存在进行赋值
if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
const { writable, configurable, enumerable } = descriptor!;
// 判断是否可写入,写入target即fakeWindow中
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
target[p] = value;
} // 如果在白名单中直接在全局赋值
if (variableWhiteList.indexOf(p) !== -1) {
globalContext[p] = value;
}
// 变更记录
updatedValueSet.add(p);
this.latestSetProp = p;
return true;
}
...
return true;
},

get部分

get: (target: FakeWindow, p: PropertyKey): any => {
this.registerRunningApp(name, proxy);
// 防止逃逸,对不同情况进行处理
if (p === Symbol.unscopables) return unscopables;
if (p === 'window' || p === 'self') {
return proxy;
} if (p === 'globalThis') {
return proxy;
} if (
p === 'top' ||
p === 'parent' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
if (globalContext === globalContext.parent) {
return proxy;
}
return (globalContext as any)[p];
} if (p === 'hasOwnProperty') {
return hasOwnProperty;
} // 直接返回document
if (p === 'document') {
return document;
} //直接返回eval
if (p === 'eval') {
return eval;
}
// 参考https://github.com/umijs/qiankun/discussions/1411
const value = propertiesWithGetter.has(p)
? (globalContext as any)[p]
: p in target
? (target as any)[p]
: (globalContext as any)[p];
// 异常的处理,调用某些api的时候会出现调用异常的情况
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
return getTargetValue(boundTarget, value);
}

其他操作的一些兼容性处理,进一步保证了沙箱的安全

has(target: FakeWindow, p: string | number | symbol): boolean {...},
getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {...},
ownKeys(target: FakeWindow): ArrayLike<string | symbol> {...},
defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {...},
deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {...},
getPrototypeOf() {...}

该模式最具优势的一点是操作基于window上拷贝的副本FakeWindow,从而保证了多个沙箱实例并行的情况。


最后

欢迎关注【袋鼠云数栈UED团队】~

袋鼠云数栈UED团队持续为广大开发者分享技术成果,相继参与开源了欢迎star

聊聊 QianKun JS 沙箱的那些事的更多相关文章

  1. 关于JS 沙箱(转)

    javascript中的沙箱并非传统意义上的沙箱,只是一种语法上的hack写法而已,javascript中处理模块依赖关系的闭包被称之为沙箱,和 ajax一样,这种sandbox coding风格是一 ...

  2. 聊聊.netcore采坑那一些事之系统时间and文件路径

    聊聊.netcore采坑那一些事之系统时间and文件路径 Hi,小伙伴大家好,最近工作比较忙,很久没有和大家分享点东西了.这个周末都加了两天班.公司的新项目都是采用.netcore来开发,在开发过程中 ...

  3. 动手写 js 沙箱

    本文由云+社区发表 作者:ivweb villainthr 市面上现在流行两种沙箱模式,一种是使用iframe,还有一种是直接在页面上使用new Function + eval进行执行. 殊途同归,主 ...

  4. 聊聊Vue.js的template编译

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/a ...

  5. js跨域那些事

    原文:http://www.cnblogs.com/rainman/archive/2011/02/20/1959325.html 什么是跨域 JavaScript出于安全方面的考虑,不允许跨域调用其 ...

  6. 聊聊Node.js 独立日漏洞

    背景 Node.js 社区近期在美国独立日周末的狂欢之时爆出漏洞 https://medium.com/@iojs/important-security-upgrades-for-node-js-an ...

  7. 聊聊Vue.js组件间通信的几种姿势

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/a ...

  8. JS中字符串那些事~

    1:字符串 JS中的任何数据类型都可以当作对象来看.所以string既是基本数据类型,又是对象. 2:声明字符串 var sStr = ‘字符串’;(常用) var oStr = new String ...

  9. JS之this那些事

    一直以来,对this的讨论都是热门话题.有人说掌握了this就掌握了JavaScript的80%,说法有点夸张,但可见this的重要性.至今记录了很多关于this的零碎笔记,今天就来个小结. 本人看过 ...

  10. 聊聊java工程师换工作那些事

    最近有个读者在苦恼一件事,那就是有大公司在挖他,他要不要从所在的小公司,跳槽到大公司,前提是两家公司的待遇基本一致.由这个问题结合自己多年的工作经验,来谈谈java工程师要不要跳槽,何时跳槽,怎么跳槽 ...

随机推荐

  1. Excel DDE Commands

    ! https://zhuanlan.zhihu.com/p/635569763 Excel DDE Commands 连接参数 Application: Excel Topic: System: 整 ...

  2. Hive执行计划之只有map阶段SQL性能分析和解读

    目录 目录 概述 1.不带函数操作的select-from-where型简单SQL 1.1执行示例 1.2 运行逻辑分析 1.3 伪代码解释 2.带普通函数和运行操作符的普通型SQL执行计划解读 2. ...

  3. Ubuntu 16.04关闭系统自动更新

    # 背景在使用阿里云ECS服务器时,服务器CPU与内存突然增大,经过排查发现是系统自动更新导致,幸运的是不是发生在业务高峰期,为了避免出现类似的情况,决定禁用系统自动更新,可以通过手动或者定时任务的方 ...

  4. GO 语言中 slice 的理解

    GO 语言中 slice 理解 为什么说 Go 语言的 slice 是引用类型,其底层实现明明是一个结构体? slice 的底层实现是一个包含三个字段的结构体:指向底层数组的指针.slice 的长度和 ...

  5. easyexce报错BeanMap$Generator

    class net.sf.cglib.core.DebuggingClassWriter overrides final method visit 这两个报错都可以在一起解决,因为这是由于Jar包冲突 ...

  6. 驱动开发:应用DeviceIoContro模板精讲

    在笔者上一篇文章<驱动开发:应用DeviceIoContro开发模板>简单为大家介绍了如何使用DeviceIoContro模板快速创建一个驱动开发通信案例,但是该案例过于简单也无法独立加载 ...

  7. Kubernetes(k8s) Web-UI界面(一):部署和访问仪表板(Dashboard)

    目录 一.系统环境 二.前言 三.仪表板(Dashboard)简介 四.部署Kubernetes仪表板(Dashboard) 五.访问Kubernetes仪表板(Dashboard) 5.1 使用to ...

  8. Maven进阶

    前言 在项目开发的过程中,我们通常要使用到外部依赖的组件,同时也会使用某些插件来帮助我们管理项目.例如,我们访问数据库的时候需要使用到jdbc组件,我们可以下载对应的jar包去加载到我们的应用中.在我 ...

  9. unity协程(IEnumerator)开始和结束

    快速阅览: 一.结束协程无效:协程方法需要单独存一份地址,在开始和结束不直接传入方法名,而是使用这份保存的地址进行开始和结束. 二.再次开启协程时少跑了几行代码:再次开始同一个方法名的协程时,不是从第 ...

  10. Chrome 报错: Unchecked runtime.lastError: Could not establish connection. Receiving end does not exist.

    经检查,是由浏览器中的插件导致的报错. 解决方案: 将该插件移除或关闭