App下載

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

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

111

一、前言

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

二、使用場景

2.1 iPaaS 可視化 API 編排  

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

2.2 微前端應用沙箱  

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

接下來的篇章我們將介紹大前端領域沙箱的實現(xiàn)以及我們如何基于JS沙箱落地應用的過程。

三、JS沙箱調研

3.1 eval和Function  

前端常見的動態(tài)執(zhí)行代碼的方式是使用 Eval 和 New Function 提供一個運行外部代碼的環(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í)行,并且返回結果相同,但是用來創(chuàng)建沙箱環(huán)境還不夠格,因為它們都能訪問[全局變量],無法實現(xiàn)作用域隔離。

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

3.2.1 with關鍵字  

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

不推薦使用 with,在 ECMAScript 5 嚴格模式中該標簽已被禁止。推薦的替代方案是聲明一個臨時變量來承載你所需要的屬性。

3.2.2 ES6 Proxy  

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

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 沙箱機制。但是如果對象的Symbol.unScopables設置為 true ,會無視 with 的作用域直接向上查找,造成沙箱逃逸,所以要另外處理 Symbol.unScopables。

3.2.4 沙箱實現(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è)服務商")') // Cannot read property 'log' of undefined
sandbox('console.log("智慧商業(yè)服務商")', {console: window.console}) // 智慧商業(yè)服務商
       

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

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

3.3 基于iframe實現(xiàn)  

iframe 標簽可以創(chuàng)造一個獨立的瀏覽器原生級別的運行環(huán)境,這個環(huán)境由瀏覽器實現(xiàn)了與主環(huán)境的隔離。在 iframe 中運行的腳本程序訪問到的全局對象均是當前 iframe 執(zhí)行上下文提供的,不會影響其父頁面的主體功能,因此使用 iframe 來實現(xià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運行時實現(xiàn)  

3.4.1 原生模塊vm  

相較于瀏覽器環(huán)境,Node運行時就簡單很多,使用其提供的原生vm模塊,可以很方便的創(chuàng)建V8虛擬機,并在指定上下文編譯和執(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 模塊不是安全機制。不要用它來運行不受信任的代碼。

3.4.2 不安全原因  

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

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

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

3.4.3 解決方案  

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

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


0 人點贊