欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

淺談 JavaScript 沙箱Sandbox

 更新時(shí)間:2021年10月29日 08:48:26   作者:ELab團(tuán)隊(duì)  
在計(jì)算機(jī)安全中,沙箱(Sandbox)是一種用于隔離正在運(yùn)行程序的安全機(jī)制,通常用于執(zhí)行未經(jīng)測(cè)試或不受信任的程序或代碼,它會(huì) 為待執(zhí)行的程序創(chuàng)建一個(gè)獨(dú)立的執(zhí)行環(huán)境,內(nèi)部程序的執(zhí)行不會(huì)影響到外部程序的運(yùn)行,下文我們來(lái)介紹一個(gè)“瀏覽器世界”的沙箱

前言:

說(shuō)到沙箱,我們的腦海中可能會(huì)條件反射地聯(lián)想到上面這個(gè)畫面并瞬間變得興致滿滿,不過(guò)很可惜本文并不涉及“我的世界”(老封面黨了),下文將逐步介紹“瀏覽器世界”的沙箱。

1、什么是沙箱

在計(jì)算機(jī)安全中, 沙箱(Sandbox)是一種用于隔離正在運(yùn)行程序的安全機(jī)制 ,通常用于執(zhí)行未經(jīng)測(cè)試或不受信任的程序或代碼,它會(huì) 為待執(zhí)行的程序創(chuàng)建一個(gè)獨(dú)立的執(zhí)行環(huán)境,內(nèi)部程序的執(zhí)行不會(huì)影響到外部程序的運(yùn)行 。

例如,下列場(chǎng)景就涉及了沙箱這一抽象的概念:

  • 我們開發(fā)的頁(yè)面程序運(yùn)行在瀏覽器中,程序只能修改瀏覽器允許我們修改的那部分接口,我們無(wú)法通過(guò)這段腳本影響到瀏覽器之外的狀態(tài),在這個(gè)場(chǎng)景下瀏覽器本身就是一個(gè)沙箱。
  • 瀏覽器中每個(gè)標(biāo)簽頁(yè)運(yùn)行一個(gè)獨(dú)立的網(wǎng)頁(yè),每個(gè)標(biāo)簽頁(yè)之間互不影響,這個(gè)標(biāo)簽頁(yè)就是一個(gè)沙箱。
  • ......

2、沙箱有什么應(yīng)用場(chǎng)景

上述介紹了一些較為宏觀的沙箱場(chǎng)景,其實(shí)在日常的開發(fā)中也存在很多的場(chǎng)景需要應(yīng)用這樣一個(gè)機(jī)制:

  • 執(zhí)行 JSONP 請(qǐng)求回來(lái)的字符串時(shí)或引入不知名第三方 JS 庫(kù)時(shí),可能需要?jiǎng)?chuàng)造一個(gè)沙箱來(lái)執(zhí)行這些代碼。
  • Vue 模板表達(dá)式的計(jì)算是運(yùn)行在一個(gè)沙盒之中的,在模板字符串中的表達(dá)式只能獲取部分全局對(duì)象,這一點(diǎn)官方文檔有提到,這一點(diǎn)官方文檔有提到,詳情可參閱源碼

  • 在線代碼編輯器,如 CodeSanbox 等在線代碼編輯器在執(zhí)行腳本時(shí)都會(huì)將程序放置在一個(gè)沙箱中,防止程序訪問/影響主頁(yè)面。
  • 許多應(yīng)用程序提供了插件(Plugin)機(jī)制,開發(fā)者可以書寫自己的插件程序?qū)崿F(xiàn)某些自定義功能。開發(fā)過(guò)插件的同學(xué)應(yīng)該知道開發(fā)插件時(shí)會(huì)有很多限制條件,這些應(yīng)用程序在運(yùn)行插件時(shí)需要遵循宿主程序制定的運(yùn)行規(guī)則,插件的運(yùn)行環(huán)境和規(guī)則就是一個(gè)沙箱。例如下圖是 Figma 插件的運(yùn)行機(jī)制:

總而言之,只要遇到不可信的第三方代碼,我們就可以使用沙箱將代碼進(jìn)行隔離,從而保障外部程序的穩(wěn)定運(yùn)行。如果不做任何處理地執(zhí)行不可信代碼,在前端中最直觀的副作用/危害就是污染、篡改全局 window 狀態(tài),影響主頁(yè)面功能甚至被 XSS 攻擊。

// 子應(yīng)用代碼

window.location.href = 'www.diaoyu.com'

Object.prototype.toString = () => {

    console.log('You are a fool :)')

  }

document.querySelectorAll('div').forEach(node => node.classList.add('hhh'))

sendRequest(document.cookie)

...

3、如何實(shí)現(xiàn)一個(gè) JS 沙箱

要實(shí)現(xiàn)一個(gè)沙箱,其實(shí)就是去制定一套程序執(zhí)行機(jī)制,在這套機(jī)制的作用下 沙箱內(nèi)部程序的運(yùn)行不會(huì)影響到外部程序的運(yùn)行 。

3.1 最簡(jiǎn)陋的沙箱

要實(shí)現(xiàn)這樣一個(gè)效果,最直接的想法就是程序中訪問的 所有變量均來(lái)自可靠或自主實(shí)現(xiàn)的上下文環(huán)境而不會(huì)從全局的執(zhí)行環(huán)境中取值, 那么要實(shí)現(xiàn)變量的訪問均來(lái)自一個(gè)可靠上下文環(huán)境,

我們需要為待執(zhí)行程序構(gòu)造一個(gè)作用域:

// 執(zhí)行上下文對(duì)象
const ctx = 
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 最簡(jiǎn)陋的沙箱
function poorestSandbox(code, ctx) {
    eval(code) // 為執(zhí)行程序構(gòu)造了一個(gè)函數(shù)作用域
}

// 待執(zhí)行程序
const code = `
    ctx.foo = 'bar'
    ctx.func(ctx.foo)
`

poorestSandbox(code, ctx) // bar

這樣的一個(gè)沙箱要求源程序在獲取任意變量時(shí)都要加上執(zhí)行上下文對(duì)象的前綴,這顯然是非常不合理的,因?yàn)槲覀儧]有辦法控制第三方的行為,是否有辦法去掉這個(gè)前綴呢?

3.2 非常簡(jiǎn)陋的沙箱(With)

使用 with聲明可以幫我們?nèi)サ暨@個(gè)前綴, with 會(huì)在作用域鏈的頂端添加一個(gè)新的作用域,該作用域的變量對(duì)象會(huì)加入 with 傳入的對(duì)象,因此相較于外部環(huán)境其內(nèi)部的代碼在查找變量時(shí)會(huì)優(yōu)先在該對(duì)象上進(jìn)行查找。

// 執(zhí)行上下文對(duì)象
const ctx = {
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 非常簡(jiǎn)陋的沙箱
function veryPoorSandbox(code, ctx) {
    with(ctx) { // Add with
        eval(code)
    }
}

// 待執(zhí)行程序
const code = `
    foo = 'bar'
    func(foo)
`

veryPoorSandbox(code, ctx) // bar

這樣一來(lái)就 實(shí)現(xiàn)了執(zhí)行程序中的變量在沙箱提供的上下文環(huán)境中查找先于外部執(zhí)行環(huán)境 的效果。

問題來(lái)了,在提供的上下文對(duì)象中沒有找到某個(gè)變量時(shí),代碼仍會(huì)沿著作用域鏈一層一層向上查找,這樣的一個(gè)沙箱仍然無(wú)法控制內(nèi)部代碼的執(zhí)行。我們 希望沙箱中的代碼只在手動(dòng)提供的上下文對(duì)象中查找變量,如果上下文對(duì)象中不存在該變量則直接報(bào)錯(cuò)或返回 undefined 。

3.3 沒那么簡(jiǎn)陋的沙箱(With + Proxy)

為了解決上述拋出的問題,我們借助 ES2015 的一個(gè)新特性—— Proxy  , Proxy 可以代理一個(gè)對(duì)象,從而攔截并定義對(duì)象的基本操作。

Proxy 中的 get 和 set 方法只能攔截已存在于代理對(duì)象中的屬性,對(duì)于代理對(duì)象中不存在的屬性這兩個(gè)鉤子是無(wú)感知的。因此這里我們使用 Proxy.has() 來(lái)攔截 with 代碼塊中的任意變量的訪問,并設(shè)置一個(gè)白名單,在白名單內(nèi)的變量可以正常走作用域鏈的訪問方式,不在白名單內(nèi)的變量會(huì)繼續(xù)判斷是否存在沙箱自行維護(hù)的上下文對(duì)象中,存在則正常訪問,不存在則直接報(bào)錯(cuò)。

由于 has 會(huì)攔截 with 代碼塊中所有的變量訪問,而我們只是想監(jiān)控被執(zhí)行代碼塊中的程序,因此還需要轉(zhuǎn)換一下手動(dòng)執(zhí)行代碼的形式 :

// 構(gòu)造一個(gè) with 來(lái)包裹需要執(zhí)行的代碼,返回 with 代碼塊的一個(gè)函數(shù)實(shí)例
function withedYourCode(code) {
  code = 'with(globalObj) {' + code + '}'
  return new Function('globalObj', code)
}


// 可訪問全局作用域的白名單列表
const access_white_list = ['Math', 'Date']


// 待執(zhí)行程序
const code = `
    Math.random()
    location.href = 'xxx'
    func(foo)
`

// 執(zhí)行上下文對(duì)象
const ctx = {
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 執(zhí)行上下文對(duì)象的代理對(duì)象
const ctxProxy = new Proxy(ctx, {
    has: (target, prop) => { // has 可以攔截 with 代碼塊中任意屬性的訪問
      if (access_white_list.includes(prop)) { // 在可訪問的白名單內(nèi),可繼續(xù)向上查找
          return target.hasOwnProperty(prop)
      }

      if (!target.hasOwnProperty(prop)) {
          throw new Error(`Invalid expression - ${prop}! You can not do that!`)
      }

      return true
    }
})

// 沒那么簡(jiǎn)陋的沙箱

function littlePoorSandbox(code, ctx) {

    withedYourCode(code).call(ctx, ctx) // 將 this 指向手動(dòng)構(gòu)造的全局代理對(duì)象

}


littlePoorSandbox(code, ctxProxy)

// Uncaught Error: Invalid expression - location! You can not do that!

到這一步,其實(shí)很多較為簡(jiǎn)單的場(chǎng)景就可以覆蓋了(eg: Vue 的模板字符串),那如果想要實(shí)現(xiàn) CodeSanbox這樣的 web 編輯器呢?在這樣的編輯器中我們可以任意使用諸如 document 、 location 等全局變量且不會(huì)影響主頁(yè)面。

從而又衍生出另一個(gè)問題——如何讓子程序使用所有全局對(duì)象的同時(shí)不影響外部的全局狀態(tài)呢?

3.4 天然的優(yōu)質(zhì)沙箱(iframe)

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

試想一個(gè)這樣的場(chǎng)景:一個(gè)頁(yè)面中有多個(gè)沙箱窗口,其中有一個(gè)沙箱需要與主頁(yè)面共享幾個(gè)全局狀態(tài)(eg: 點(diǎn)擊瀏覽器回退按鈕時(shí)子應(yīng)用也會(huì)跟隨著回到上一級(jí)),另一個(gè)沙箱需要與主頁(yè)面共享另外一些全局狀態(tài)(eg: 共享 cookie 登錄態(tài))。

雖然瀏覽器為主頁(yè)面和 iframe 之間提供了 postMessage 等方式進(jìn)行通信,但單單使用 iframe 來(lái)實(shí)現(xiàn)這個(gè)場(chǎng)景是比較困難且不易維護(hù)的。

3.5應(yīng)該能用的沙箱(With + Proxy + iframe)

為了實(shí)現(xiàn)上述場(chǎng)景,我們把上述方法縫合一下即可:

  • 利用 iframe 對(duì)全局對(duì)象的天然隔離性,將 iframe.contentWindow 取出作為當(dāng)前沙箱執(zhí)行的全局對(duì)象
  • 將上述沙箱全局對(duì)象作為 with 的參數(shù)限制內(nèi)部執(zhí)行程序的訪問,同時(shí)使用 Proxy 監(jiān)聽程序內(nèi)部的訪問。
  • 維護(hù)一個(gè)共享狀態(tài)列表,列出需要與外部共享的全局狀態(tài),在 Proxy 內(nèi)部實(shí)現(xiàn)訪問控制。
// 沙箱全局代理對(duì)象類
class SandboxGlobalProxy {

    constructor(sharedState) {
        // 創(chuàng)建一個(gè) iframe 對(duì)象,取出其中的原生瀏覽器全局對(duì)象作為沙箱的全局對(duì)象
        const iframe = document.createElement('iframe', {url: 'about:blank'})
        document.body.appendChild(iframe)
        const sandboxGlobal = iframe.contentWindow // 沙箱運(yùn)行時(shí)的全局對(duì)象
    

        return new Proxy(sandboxGlobal, {
            has: (target, prop) => { // has 可以攔截 with 代碼塊中任意屬性的訪問
                if (sharedState.includes(prop)) { // 如果屬性存在于共享的全局狀態(tài)中,則讓其沿著原型鏈在外層查找
                    return false
                }

                if (!target.hasOwnProperty(prop)) {
                    throw new Error(`Invalid expression - ${prop}! You can not do that!`)
                }
                return true
            }
        })

    }

}


function maybeAvailableSandbox(code, ctx) {

    withedYourCode(code).call(ctx, ctx)

}

const code_1 = `

    console.log(history == window.history) // false

    window.abc = 'sandbox'

    Object.prototype.toString = () => {

        console.log('Traped!')

    }

    console.log(window.abc) // sandbox

`

const sharedGlobal_1 = ['history'] // 希望與外部執(zhí)行環(huán)境共享的全局對(duì)象

const globalProxy_1 = new SandboxGlobalProxy(sharedGlobal_1)

maybeAvailableSandbox(code_1, globalProxy_1)


window.abc // undefined

Object.prototype.toString() // [object Object] 并沒有打印 Traped

從實(shí)例代碼的結(jié)果可以看到借用 iframe 天然的環(huán)境隔離優(yōu)勢(shì)和 with + Proxy 強(qiáng)大的控制力,我們實(shí)現(xiàn)了沙箱內(nèi)全局對(duì)象和外層的全局對(duì)象的隔離,并實(shí)現(xiàn)了共享部分全局屬性。

3.6 沙箱逃逸(Sandbox Escape)

沙箱于作者而言是一種安全策略,但于使用者而言可能是一種束縛。腦洞大開的開發(fā)者們嘗試用各種方式擺脫這種束縛,也稱之為 沙箱逃逸 。因此一個(gè)沙箱程序最大的挑戰(zhàn)就是如何檢測(cè)并禁止這些預(yù)期之外的程序執(zhí)行。

上面實(shí)現(xiàn)的沙箱似乎已經(jīng)滿足了我們的功能,大功告成了嗎?其實(shí)不然,下列操作均會(huì)對(duì)沙箱之外的環(huán)境造成影響,實(shí)現(xiàn)沙箱逃逸:

訪問沙箱執(zhí)行上下文中某個(gè)對(duì)象內(nèi)部屬性時(shí), Proxy 無(wú)法捕獲到這個(gè)屬性的訪問操作 。例如我們可以直接在沙箱的執(zhí)行上下文中通過(guò) window.parent 拿到外層的全局對(duì)象。

// 訪問沙箱對(duì)象中對(duì)象的屬性時(shí),省略了上文中的部分代碼

const ctx = {

    window: {

        parent: {...},

        ...

    }

}

const code = `

    window.parent.abc = 'xxx'

`

window.abc // xxx

  • 通過(guò)訪問原型鏈實(shí)現(xiàn)逃逸,JS 可以直接聲明一個(gè)字面量,沿著該字面量的原型鏈向上查找原型對(duì)象即可訪問到外層的全局對(duì)象,這種行為亦是無(wú)法感知的。
const code = `

    ({}).constructor.prototype.toString = () => {

        console.log('Escape!')

    }

`

({}).toString() // Escape!  預(yù)期是 [object Object]

3.7 “無(wú)瑕疵”的沙箱(Customize Interpreter)

通過(guò)上述的種種方式來(lái)實(shí)現(xiàn)一個(gè)沙箱或多或少存在一些缺陷,那是否存在一個(gè)趨于完備的沙箱呢?

其實(shí)有不少開源庫(kù)已經(jīng)在做這樣一件事情,也就是分析源程序結(jié)構(gòu)從而手動(dòng)控制每一條語(yǔ)句的執(zhí)行邏輯,通過(guò)這樣一種方式無(wú)論是指定程序運(yùn)行時(shí)的上下文環(huán)境還是捕獲妄想逃脫沙箱控制的操作都是在掌控范圍內(nèi)的。實(shí)現(xiàn)這樣一個(gè)沙箱本質(zhì)上就是實(shí)現(xiàn)一個(gè)自定義的解釋器。

function almostPerfectSandbox(code, ctx, illegalOperations) {

    return myInterpreter(code, ctx, illegalOperations) // 自定義解釋器

}

4、總結(jié)

本文主要介紹了沙箱的基本概念、應(yīng)用場(chǎng)景以及引導(dǎo)各位思考如何去實(shí)現(xiàn)一個(gè) JavaScript 沙箱。沙箱的實(shí)現(xiàn)方式并不是一成不變的,應(yīng)當(dāng)結(jié)合具體的場(chǎng)景分析其需要達(dá)成的目標(biāo)。除此之外,沙箱逃逸的防范同樣是一件任重而道遠(yuǎn)的事,因?yàn)楹茈y在構(gòu)建的初期就覆蓋所有的執(zhí)行 case。

沒有一個(gè)沙箱的組裝是一蹴而就的,就像“我的世界”一樣。

5、參考

參考資料:

源碼: https://github.com/vuejs/vue/blob/v2.6.10/src/core/instance/proxy.js
CodeSanbox: https://codesandbox.io/
with: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/with
Proxy: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy
CodeSanbox: https://codesandbox.io/
Writing a JavaScript framework - Sandboxed Code Evaluation: https://blog.risingstack.com/writing-a-javascript-framework-sandboxed-code-evaluation/
說(shuō)說(shuō) JS 中的沙箱: https://juejin.cn/post/6844903954074058760#heading-1

相關(guān)文章

最新評(píng)論