首頁 >  軟體

淺談 JavaScript 沙箱Sandbox

2021-10-29 13:00:56

前言:

說到沙箱,我們的腦海中可能會條件反射地聯想到上面這個畫面並瞬間變得興致滿滿,不過很可惜本文並不涉及「我的世界」(老封面黨了),下文將逐步介紹「瀏覽器世界」的沙箱。

1、什麼是沙箱

在電腦保安中, 沙箱(Sandbox)是一種用於隔離正在執行程式的安全機制 ,通常用於執行未經測試或不受信任的程式或程式碼,它會 為待執行的程式建立一個獨立的執行環境,內部程式的執行不會影響到外部程式的執行 。

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

  • 我們開發的頁面程式執行在瀏覽器中,程式只能修改瀏覽器允許我們修改的那部分介面,我們無法通過這段指令碼影響到瀏覽器之外的狀態,在這個場景下瀏覽器本身就是一個沙箱。
  • 瀏覽器中每個分頁執行一個獨立的網頁,每個分頁之間互不影響,這個分頁就是一個沙箱。
  • ......

2、沙箱有什麼應用場景

上述介紹了一些較為宏觀的沙箱場景,其實在日常的開發中也存在很多的場景需要應用這樣一個機制:

  • 執行 JSONP 請求回來的字串時或引入不知名第三方 JS 庫時,可能需要創造一個沙箱來執行這些程式碼。
  • Vue 模板表示式的計算是執行在一個沙盒之中的,在模板字串中的表示式只能獲取部分全域性物件,這一點官方檔案有提到,這一點官方檔案有提到,詳情可參閱 原始碼

  • 線上程式碼編輯器,如 CodeSanbox 等線上程式碼編輯器在執行指令碼時都會將程式放置在一個沙箱中,防止程式存取/影響主頁面。
  • 許多應用程式提供了外掛(Plugin)機制,開發者可以書寫自己的外掛程式實現某些自定義功能。開發過外掛的同學應該知道開發外掛時會有很多限制條件,這些應用程式在執行外掛時需要遵循宿主程式制定的執行規則,外掛的執行環境和規則就是一個沙箱。例如下圖是 Figma 外掛的執行機制:

總而言之,只要遇到不可信的第三方程式碼,我們就可以使用沙箱將程式碼進行隔離,從而保障外部程式的穩定執行。如果不做任何處理地執行不可信程式碼,在前端中最直觀的副作用/危害就是汙染、篡改全域性 window 狀態,影響主頁面功能甚至被 XSS 攻擊。

// 子應用程式碼

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、如何實現一個 JS 沙箱

要實現一個沙箱,其實就是去制定一套程式執行機制,在這套機制的作用下 沙箱內部程式的執行不會影響到外部程式的執行 。

3.1 最簡陋的沙箱

要實現這樣一個效果,最直接的想法就是程式中存取的 所有變數均來自可靠或自主實現的上下文環境而不會從全域性的執行環境中取值, 那麼要實現變數的存取均來自一個可靠上下文環境,

我們需要為待執行程式構造一個作用域:

// 執行上下文物件
const ctx = 
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 最簡陋的沙箱
function poorestSandbox(code, ctx) {
    eval(code) // 為執行程式構造了一個函數作用域
}

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

poorestSandbox(code, ctx) // bar

這樣的一個沙箱要求源程式在獲取任意變數時都要加上執行上下文物件的字首,這顯然是非常不合理的,因為我們沒有辦法控制第三方的行為,是否有辦法去掉這個字首呢?

3.2 非常簡陋的沙箱(With)

使用 with宣告可以幫我們去掉這個字首, with 會在作用域鏈的頂端新增一個新的作用域,該作用域的變數物件會加入 with 傳入的物件,因此相較於外部環境其內部的程式碼在查詢變數時會優先在該物件上進行查詢。

// 執行上下文物件
const ctx = {
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

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

// 待執行程式
const code = `
    foo = 'bar'
    func(foo)
`

veryPoorSandbox(code, ctx) // bar

這樣一來就 實現了執行程式中的變數在沙箱提供的上下文環境中查詢先於外部執行環境 的效果。

問題來了,在提供的上下文物件中沒有找到某個變數時,程式碼仍會沿著作用域鏈一層一層向上查詢,這樣的一個沙箱仍然無法控制內部程式碼的執行。我們 希望沙箱中的程式碼只在手動提供的上下文物件中查詢變數,如果上下文物件中不存在該變數則直接報錯或返回 undefined

3.3 沒那麼簡陋的沙箱(With + Proxy)

為了解決上述丟擲的問題,我們藉助 ES2015 的一個新特性—— Proxy  Proxy 可以代理一個物件,從而攔截並定義物件的基本操作。

Proxy 中的 get 和 set 方法只能攔截已存在於代理物件中的屬性,對於代理物件中不存在的屬性這兩個勾點是無感知的。因此這裡我們使用 Proxy.has() 來攔截 with 程式碼塊中的任意變數的存取,並設定一個白名單,在白名單內的變數可以正常走作用域鏈的存取方式,不在白名單內的變數會繼續判斷是否存在沙箱自行維護的上下文物件中,存在則正常存取,不存在則直接報錯。

由於 has 會攔截 with 程式碼塊中所有的變數存取,而我們只是想監控被執行程式碼塊中的程式,因此還需要轉換一下手動執行程式碼的形式 :

// 構造一個 with 來包裹需要執行的程式碼,返回 with 程式碼塊的一個函數範例
function withedYourCode(code) {
  code = 'with(globalObj) {' + code + '}'
  return new Function('globalObj', code)
}


// 可存取全域性作用域的白名單列表
const access_white_list = ['Math', 'Date']


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

// 執行上下文物件
const ctx = {
    func: variable => {
        console.log(variable)
    },
    foo: 'foo'
}

// 執行上下文物件的代理物件
const ctxProxy = new Proxy(ctx, {
    has: (target, prop) => { // has 可以攔截 with 程式碼塊中任意屬性的存取
      if (access_white_list.includes(prop)) { // 在可存取的白名單內,可繼續向上查詢
          return target.hasOwnProperty(prop)
      }

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

      return true
    }
})

// 沒那麼簡陋的沙箱

function littlePoorSandbox(code, ctx) {

    withedYourCode(code).call(ctx, ctx) // 將 this 指向手動構造的全域性代理物件

}


littlePoorSandbox(code, ctxProxy)

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

到這一步,其實很多較為簡單的場景就可以覆蓋了(eg: Vue 的模板字串),那如果想要實現 CodeSanbox這樣的 web 編輯器呢?在這樣的編輯器中我們可以任意使用諸如 document location 等全域性變數且不會影響主頁面。

從而又衍生出另一個問題——如何讓子程式使用所有全域性物件的同時不影響外部的全域性狀態呢?

3.4 天然的優質沙箱(iframe)

聽到上面這個問題 iframe 直呼內行, iframe 標籤可以創造一個獨立的瀏覽器原生級別的執行環境,這個環境由瀏覽器實現了與主環境的隔離。在 iframe 中執行的指令碼程式存取到的全域性物件均是當前 iframe 執行上下文提供的,不會影響其父頁面的主體功能,因此 使用 iframe 來實現一個沙箱是目前最方便、簡單、安全的方法 。

試想一個這樣的場景:一個頁面中有多個沙箱視窗,其中有一個沙箱需要與主頁面共用幾個全域性狀態(eg: 點選瀏覽器回退按鈕時子應用也會跟隨著回到上一級),另一個沙箱需要與主頁面共用另外一些全域性狀態(eg: 共用 cookie 登入態)。

雖然瀏覽器為主頁面和 iframe 之間提供了 postMessage 等方式進行通訊,但單單使用 iframe 來實現這個場景是比較困難且不易維護的。

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

為了實現上述場景,我們把上述方法縫合一下即可:

  • 利用 iframe 對全域性物件的天然隔離性,將 iframe.contentWindow 取出作為當前沙箱執行的全域性物件
  • 將上述沙箱全域性物件作為 with 的引數限制內部執行程式的存取,同時使用 Proxy 監聽程式內部的存取。
  • 維護一個共用狀態列表,列出需要與外部共用的全域性狀態,在 Proxy 內部實現存取控制。
// 沙箱全域性代理物件類
class SandboxGlobalProxy {

    constructor(sharedState) {
        // 建立一個 iframe 物件,取出其中的原生瀏覽器全域性物件作為沙箱的全域性物件
        const iframe = document.createElement('iframe', {url: 'about:blank'})
        document.body.appendChild(iframe)
        const sandboxGlobal = iframe.contentWindow // 沙箱執行時的全域性物件
    

        return new Proxy(sandboxGlobal, {
            has: (target, prop) => { // has 可以攔截 with 程式碼塊中任意屬性的存取
                if (sharedState.includes(prop)) { // 如果屬性存在於共用的全域性狀態中,則讓其沿著原型鏈在外層查詢
                    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'] // 希望與外部執行環境共用的全域性物件

const globalProxy_1 = new SandboxGlobalProxy(sharedGlobal_1)

maybeAvailableSandbox(code_1, globalProxy_1)


window.abc // undefined

Object.prototype.toString() // [object Object] 並沒有列印 Traped

從範例程式碼的結果可以看到借用 iframe 天然的環境隔離優勢和 with + Proxy 強大的控制力,我們實現了沙箱內全域性物件和外層的全域性物件的隔離,並實現了共用部分全域性屬性。

3.6 沙箱逃逸(Sandbox Escape)

沙箱於作者而言是一種安全策略,但於使用者而言可能是一種束縛。腦洞大開的開發者們嘗試用各種方式擺脫這種束縛,也稱之為 沙箱逃逸 。因此一個沙箱程式最大的挑戰就是如何檢測並禁止這些預期之外的程式執行。

上面實現的沙箱似乎已經滿足了我們的功能,大功告成了嗎?其實不然,下列操作均會對沙箱之外的環境造成影響,實現沙箱逃逸:

存取沙箱執行上下文中某個物件內部屬性時, Proxy 無法捕獲到這個屬性的存取操作 。例如我們可以直接在沙箱的執行上下文中通過 window.parent 拿到外層的全域性物件。

// 存取沙箱物件中物件的屬性時,省略了上文中的部分程式碼

const ctx = {

    window: {

        parent: {...},

        ...

    }

}

const code = `

    window.parent.abc = 'xxx'

`

window.abc // xxx

  • 通過存取原型鏈實現逃逸,JS 可以直接宣告一個字面量,沿著該字面量的原型鏈向上查詢原型物件即可存取到外層的全域性物件,這種行為亦是無法感知的。
const code = `

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

        console.log('Escape!')

    }

`

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

3.7 「無瑕疵」的沙箱(Customize Interpreter)

通過上述的種種方式來實現一個沙箱或多或少存在一些缺陷,那是否存在一個趨於完備的沙箱呢?

其實有不少開源庫已經在做這樣一件事情,也就是分析源程式結構從而手動控制每一條語句的執行邏輯,通過這樣一種方式無論是指定程式執行時的上下文環境還是捕獲妄想逃脫沙箱控制的操作都是在掌控範圍內的。實現這樣一個沙箱本質上就是實現一個自定義的直譯器。

function almostPerfectSandbox(code, ctx, illegalOperations) {

    return myInterpreter(code, ctx, illegalOperations) // 自定義直譯器

}

4、總結

本文主要介紹了沙箱的基本概念、應用場景以及引導各位思考如何去實現一個 JavaScript 沙箱。沙箱的實現方式並不是一成不變的,應當結合具體的場景分析其需要達成的目標。除此之外,沙箱逃逸的防範同樣是一件任重而道遠的事,因為很難在構建的初期就覆蓋所有的執行 case

沒有一個沙箱的組裝是一蹴而就的,就像「我的世界」一樣。

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/
說說 JS 中的沙箱: https://juejin.cn/post/6844903954074058760#heading-1


IT145.com E-mail:sddin#qq.com

代做工资流水公司吉林办工资流水单沧州企业贷流水开具长春房贷流水公司柳州企业流水打印多少钱湖州打贷款流水淄博企业流水打印报价深圳薪资流水报价无锡办银行流水单曲靖自存银行流水样本邯郸薪资流水单代开合肥个人流水查询常德入职工资流水代办烟台背调工资流水价格开封房贷流水代开揭阳购房银行流水办理湛江查个人银行流水南昌工资流水账单打印泉州薪资流水开具保定房贷工资流水 代开西安打薪资流水漳州企业对公流水图片宿迁办理贷款银行流水鞍山离职证明代做金华银行对公流水模板烟台工作收入证明查询咸阳银行对公流水价格黄冈打贷款工资流水天津签证流水开具泉州薪资银行流水制作贵阳代办薪资银行流水香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

代做工资流水公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化