App下載

微前端是如何實(shí)現(xiàn)作用域隔離的?

蔡文姬腿堡 2024-04-16 14:12:52 瀏覽數(shù) (1405)
反饋

111

一、前言

沙箱(Sandbox)是一種安全機(jī)制,目的是讓程序運(yùn)行在一個(gè)相對(duì)獨(dú)立的隔離環(huán)境,使其不對(duì)外界的程序造成影響,保障系統(tǒng)的安全。作為開(kāi)發(fā)人員,我們經(jīng)常會(huì)同沙箱環(huán)境打交道,例如,服務(wù)器中使用 Docker 創(chuàng)建應(yīng)用容器;使用 Codesandbox運(yùn)行 Demo示例;在程序中創(chuàng)建沙箱執(zhí)行動(dòng)態(tài)腳本等。

二、使用場(chǎng)景

2.1 iPaaS 可視化 API 編排  

在流程編排的某些節(jié)點(diǎn)需要用到低代碼模型轉(zhuǎn)換(Transformer),用戶可在轉(zhuǎn)換器流程節(jié)點(diǎn)自定義 Groovy 腳本實(shí)現(xiàn),服務(wù)端在執(zhí)行自定義的 Groovy 腳本時(shí),會(huì)放置在沙箱中,避免對(duì)整個(gè)流程邏輯造成影響。

2.2 微前端應(yīng)用沙箱  

在微前端當(dāng)中,有一些全局對(duì)象在所有的應(yīng)用中需要共享,如 Window 對(duì)象。不同開(kāi)發(fā)團(tuán)隊(duì)的子應(yīng)用很難通過(guò)規(guī)范約束他們使用全局變量。為了保證應(yīng)用的可靠性,需要技術(shù)手段去治理運(yùn)行時(shí)的沖突問(wèn)題;通過(guò)使用沙箱,每個(gè)前端應(yīng)用都可以擁有自己的上下文環(huán)境、頁(yè)面路由和狀態(tài)管理,而不會(huì)相互干擾或沖突。

接下來(lái)的篇章我們將介紹大前端領(lǐng)域沙箱的實(shí)現(xiàn)以及我們?nèi)绾位贘S沙箱落地應(yīng)用的過(guò)程。

三、JS沙箱調(diào)研

3.1 eval和Function  

前端常見(jiàn)的動(dòng)態(tài)執(zhí)行代碼的方式是使用 Eval 和 New Function 提供一個(gè)運(yùn)行外部代碼的環(huán)境:     

// 使用 eval 的糟糕代碼:
function looseJsonParse(obj){
    return eval(`(${obj})`);
}
console.log(looseJsonParse(
   "{a:(4-1), b:function(){}, c:new Date()}"
))

// 使用 Function 的更好的代碼:
function looseJsonParse(obj){
    return Function(`"use strict";return (${obj})`)();
}
console.log(looseJsonParse(
   "{a:(4-1), b:function(){}, c:new Date()}"
))

兩種方式都可以正常執(zhí)行,并且返回結(jié)果相同,但是用來(lái)創(chuàng)建沙箱環(huán)境還不夠格,因?yàn)樗鼈兌寄茉L問(wèn)[全局變量],無(wú)法實(shí)現(xiàn)作用域隔離。

3.2 with + new Function + proxy實(shí)現(xiàn)  

3.2.1 with關(guān)鍵字  

JavaScript 在查找某個(gè)未使用命名空間的變量時(shí),會(huì)通過(guò)作用于鏈來(lái)查找,而 with 關(guān)鍵字,可以使得查找時(shí),先從該對(duì)象的屬性開(kāi)始查找,若該對(duì)象沒(méi)有要查找的屬性,順著上一級(jí)作用域鏈查找,若不存在要查到的屬性,則會(huì)返回 ReferenceError 異常。

不推薦使用 with,在 ECMAScript 5 嚴(yán)格模式中該標(biāo)簽已被禁止。推薦的替代方案是聲明一個(gè)臨時(shí)變量來(lái)承載你所需要的屬性。

3.2.2 ES6 Proxy  

Proxy 是 ES6 提供的新語(yǔ)法,Proxy 對(duì)象用于創(chuàng)建一個(gè)對(duì)象的代理,從而實(shí)現(xiàn)基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。示例如下:

const handler = {
  get: function (obj, prop) {
    return prop in obj ? obj[prop] : 'weimob';
  },
};

const p = new Proxy({}, handler);
p.a = 2023;
p.b = undefined;

console.log(p.a, p.b); // 2023 undefined
console.log('c' in p, p.c); // false, weimob    

3.2.3 Symbol.unScopables  

With 再加上 Proxy 幾乎完美解決 JS 沙箱機(jī)制。但是如果對(duì)象的Symbol.unScopables設(shè)置為 true ,會(huì)無(wú)視 with 的作用域直接向上查找,造成沙箱逃逸,所以要另外處理 Symbol.unScopables。

3.2.4 沙箱實(shí)現(xiàn)   

function sandbox(code, context) {
  context = context || Object.create(null);
  const fn = new Function('context', `with(context){return (${code})}`);
  const proxy = new Proxy(context, {
    has(target, key) {
      if (["console", "setTimeout", "Date"].includes(key)) {
        return true
      }
      if (!target.hasOwnProperty(key)) {
        throw new Error(`Illegal operation for key ${key}`)
      }
      return target[key]
    },
    get(target, key, receiver) {
      if (key === Symbol.unscopables) {
        return undefined;
      }
      return Reflect.get(target, key, receiver);
    }
  })
  return fn.call(proxy, proxy);
}

sandbox('3+2') // 5
sandbox('console.log("智慧商業(yè)服務(wù)商")') // Cannot read property 'log' of undefined
sandbox('console.log("智慧商業(yè)服務(wù)商")', {console: window.console}) // 智慧商業(yè)服務(wù)商
       

上面的代碼主要做了3件事,實(shí)現(xiàn)沙箱隔離:

  • 使用 with API,將對(duì)象添加到作用域鏈的頂部,變量訪問(wèn)會(huì)優(yōu)先查找你傳入的參數(shù)對(duì)象,之后再往上找;
  • 通過(guò)ES6提供的proxy,設(shè)置has函數(shù),實(shí)現(xiàn)對(duì)象的訪問(wèn)攔截,同時(shí)處理Symbol.unscopables 的屬性,控制可以被訪問(wèn)的變量 context,阻斷沙箱內(nèi)的對(duì)外訪問(wèn);
  • 綁定 this 指向 proxy 對(duì)象,防止 this 訪問(wèn) window;

3.3 基于iframe實(shí)現(xiàn)  

iframe 標(biāo)簽可以創(chuàng)造一個(gè)獨(dú)立的瀏覽器原生級(jí)別的運(yùn)行環(huán)境,這個(gè)環(huán)境由瀏覽器實(shí)現(xiàn)了與主環(huán)境的隔離。在 iframe 中運(yùn)行的腳本程序訪問(wèn)到的全局對(duì)象均是當(dāng)前 iframe 執(zhí)行上下文提供的,不會(huì)影響其父頁(yè)面的主體功能,因此使用 iframe 來(lái)實(shí)現(xiàn)一個(gè)沙箱是目前最方便、簡(jiǎn)單、安全的方法。

const parent = window;
const frame = document.createElement('iframe');
// 限制代碼 iframe 代碼執(zhí)行能力
frame.sandbox = 'allow-same-origin';
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;

3.4 node運(yùn)行時(shí)實(shí)現(xiàn)  

3.4.1 原生模塊vm  

相較于瀏覽器環(huán)境,Node運(yùn)行時(shí)就簡(jiǎn)單很多,使用其提供的原生vm模塊,可以很方便的創(chuàng)建V8虛擬機(jī),并在指定上下文編譯和執(zhí)行代碼;

const vm = require('node:vm');

const x = 1;

const context = { x: 2 };
vm.createContext(context); // Contextify the object.

const code = 'x += 40; var y = 17;';
vm.runInContext(code, context);

console.log(context.x); // 42
console.log(context.y); // 17

console.log(x); // 1; y is not defined.

問(wèn)題來(lái)了,使用 vm.runInContext 看似創(chuàng)建了沙箱隔離環(huán)境,但 vm 模塊足夠安全嗎?引用 Node 官網(wǎng)的回答

node:vm 模塊不是安全機(jī)制。不要用它來(lái)運(yùn)行不受信任的代碼。

3.4.2 不安全原因  

為什么不是安全機(jī)制,繼續(xù)剖析;

const vm = require('vm');
vm.runInNewContext('this.constructor.constructor("return process")().exit()');
console.log('智慧商業(yè)服務(wù)商') // 永遠(yuǎn)不會(huì)執(zhí)行

這就是 JS 語(yǔ)言的特性,以上示例中 runInNewContext 會(huì)默認(rèn)創(chuàng)建上下文對(duì)象, this 指向默認(rèn)創(chuàng)建的 ctx 對(duì)象 并通過(guò)原型鏈的方式拿到沙盒外的 Funtion,通過(guò)Function 訪問(wèn)全局變量,完成逃逸,并執(zhí)行逃逸后的 JS 代碼。

3.4.3 解決方案  

解決方案是綁定上下文對(duì)象,同時(shí)切斷上下文對(duì)象的原型鏈,提供純凈的上下文對(duì)象,避免通過(guò)原型鏈逃逸。

const vm = require('vm');
let sandBox = Object.create(null);
sandBox.title = '智慧商業(yè)服務(wù)商'
sandBox.console = console
vm.runInNewContext('console.log(title)', sandBox);


0 人點(diǎn)贊