App下載

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

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

111

一、前言

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

二、使用場景

2.1 iPaaS 可視化 API 編排  

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

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

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

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

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

3.1 eval和Function  

前端常見的動(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é)果相同,但是用來創(chuàng)建沙箱環(huán)境還不夠格,因?yàn)樗鼈兌寄茉L問[全局變量],無法實(shí)現(xiàn)作用域隔離。

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

3.2.1 with關(guān)鍵字  

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

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

3.2.2 ES6 Proxy  

Proxy 是 ES6 提供的新語法,Proxy 對象用于創(chuàng)建一個(gè)對象的代理,從而實(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ī)制。但是如果對象的Symbol.unScopables設(shè)置為 true ,會無視 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,將對象添加到作用域鏈的頂部,變量訪問會優(yōu)先查找你傳入的參數(shù)對象,之后再往上找;
  • 通過ES6提供的proxy,設(shè)置has函數(shù),實(shí)現(xiàn)對象的訪問攔截,同時(shí)處理Symbol.unscopables 的屬性,控制可以被訪問的變量 context,阻斷沙箱內(nèi)的對外訪問;
  • 綁定 this 指向 proxy 對象,防止 this 訪問 window;

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

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

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í)就簡單很多,使用其提供的原生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.

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

node:vm 模塊不是安全機(jī)制。不要用它來運(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)不會執(zhí)行

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

3.4.3 解決方案  

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

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)贊