关于前端:聊聊-QianKun-JS-沙箱的那些事

咱们是袋鼠云数栈 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的更多详情:

  • https://fjolt.com/article/javascript-shadowrealms
  • https://github.com/tc39/proposal-shadowrealm#APITypeScriptFormat

Portals

相似于iframe的新标签。
对于portals的更多详情

  • https://github.com/WICG/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

  • 大数据分布式任务调度零碎——Taier
  • 轻量级的 Web IDE UI 框架——Molecule
  • 针对大数据畛域的 SQL Parser 我的项目——dt-sql-parser
  • 袋鼠云数栈前端团队代码评审工程实际文档——code-review-practices
  • 一个速度更快、配置更灵便、应用更简略的模块打包器——ko

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理