如何安全的運行第三方javascript代碼(中)

2021-02-19 嘶吼專業版

(接上文)

在主線程上運行的含義

在我們深入進行第二種嘗試之前,我們需要先退一步,並重新考察允許插件在主線程上運行到底意味著什麼。畢竟,我們一開始並沒有考慮它,因為我們知道這可能是危險的。在主線程上運行聽起來很像eval(UNSAFE_CODE)方式。

在主線程上運行的好處是插件可以:

1.直接編輯文檔而不是副本,避免加載時間問題。

2.可以運行複雜的組件更新和約束邏輯,而無需為代碼置辦兩個副本。

3.在需要同步API時,可以使用同步API調用。這樣的話,更新的加載或刷新就不會發生混淆。

4.以更直觀的方式編寫代碼:插件只是自動執行用戶可以使用UI手動執行的操作。

但是,這時我們又遇到了下列問題:

1.插件可掛起,但無法中斷插件。

2.插件可以像figma.com一樣發出網絡請求。

3.插件可以訪問和修改全局狀態,例如修改UI,甚至可以執行惡意操作,例如修改({}).__proto__的值,從而危害所有新建的和現有的JavaScript對象。

經過斟酌之後,我們決定放棄第1項要求。當插件被凍結時,會影響Figma的穩定性。然而,我們的插件模型的工作原理是,它們只處理顯式的用戶操作。通過在插件運行時更改UI,凍結將始終被認為是插件所致。這也意味著插件無法「破壞」文檔。

eval的危險性體現在哪些方面?

為了解決插件能夠發出網絡請求和訪問全局狀態的問題,我們必須首先確切地了解「通過eval函數執行任意JavaScript代碼是危險的」這句話到底意味著什麼。

對於某些只能進行7*24*60*60這樣的算術運算的JavaScript變體,我們稱之為SimpleScript,那麼使用eval方法的話還是很安全的。

如果繼續為SimpleScript添加其他特性,如變量賦值和if語句,使其更像程式語言,這時它仍然非常安全。歸根結底,它本質上仍然歸結為做算術。如果繼續添加函數求值(function evaluation)特性,現在該語言就具備了λ演算和圖靈完備性。

換句話說,JavaScript未必一定就是危險的。在最簡化的形式中,它只是一種做算術的擴展方式。真正的危險源是它的輸入和輸出訪問權限,其中包括網絡訪問、DOM訪問等,即危險的是瀏覽器的應用程式接口。

我們知道,API都是全局變量,因此,我們需要隱藏全局變量!

隱藏全局變量

現在,隱藏全局變量在理論上聽起來不錯,但僅通過「隱藏」它們來創建安全的實現還是很困難的。例如,我們可以考慮刪除window對象的所有屬性,或將它們設置為null,但代碼仍然可以訪問全局值,例如({}).constructor。所以,找出洩漏全局變量值得所有可能方式是非常具有挑戰性的。

相反,我們需要一些更強大的沙箱形式,使得這些全局變量值從一開始就不存在。

換句話說,JavaScript並不一定非常危險。

考慮前面介紹的僅支持算術的SimpleScript語言,大家可以試著編寫一個算術運算程序。在該程序的任何合理實現中,SimpleScript將無法執行除算術之外的任何操作。

現在,我們繼續擴展SimpleScript,使其支持更多語言功能,直到它變成JavaScript為止,現在,我們將該程序稱為解釋器,它決定了JavaScript(動態解釋語言)的運行方式。

嘗試#2:將JavaScript解釋器編譯為WebAssembly

對於像我們這樣的小型創業公司來說,實現JavaScript編譯器是不太現實的。相反,為了驗證這種方法,我們採用了Duktape,這是一個用C++編寫的輕量級JavaScript解釋器,並將其編譯為WebAssembly。

為了確認它是否有效,我們運行了test262測試,它是標準的JavaScript測試套件。它通過了所有ES5測試,只有少量不重要的測試失敗了。要使用Duktape運行插件代碼,我們需要使用編譯為WebAssembly的解釋器來調用eval函數。

這種方法有哪些特性?

這個解釋器在主線程中運行,這意味著我們可以創建一個基於主線程的API。

它是安全的,因為Duktape不支持任何瀏覽器API,此外,它是作為WebAssembly運行的,而後者是一個無法訪問瀏覽器API的沙箱環境。換句話說,默認情況下,插件代碼只能通過顯式的白名單API與外界進行通信。

它比常規JavaScript的速度要慢,因為這個解釋器不支持JIT,但這並不重要。

它需要瀏覽器編譯一個中等大小的WASM二進位文件,這需要一些開銷。

默認情況下,瀏覽器調試工具無法使用,但我們花了一天時間為解釋器實現了一個控制臺,以驗證它至少可以調試插件。

Duktape僅支持ES5,但在Web社區中,通常會使用[Babel](https://babeljs.io/)等工具交叉編譯較新的JavaScript版本。

(提示:幾個月後,Fabrice Bellard發布了[QuickJS](https://bellard.org/quickjs/),它原生支持ES6。)

現在,我們要編譯一個JavaScript解釋器!根據你作為程式設計師的愛好或審美傾向,您可能會想:

這太棒了!

或者

……這是要搞啥?還要自己搞JavaScript引擎,那作業系統是不是也要自己搞一個呀?

當然,這些質疑聲是非常正常的!除非我們有絕對的必要,否則最好避免重新實現瀏覽器。在實現整個渲染系統方面,我們花費的大量的精力,因為這對於性能和跨瀏覽器支持來說是非常必要的,並且令人高興的是,我們的確做到了,但我們仍然要鄭重對讀者說一聲:不重新發明輪子。

注意,這並非我們最終採用的方法,因為後面還有更好的方法。那我們為什麼要在這裡介紹它呢?這是因為,這對於理解我們最終沙箱模型來說是非常有幫助的,畢竟我們的模型是非常複雜的。

嘗試#3:Realms

雖然編譯JS解釋器是一種很有前途的方法,但除此之外,還有一個方法非常需要考慮——Realms shim技術,其創建者為Agoric。

這項技術將創建沙箱和支持插件描述為潛在的用例。這真是一種前途無量的描述方法!Realms API看起來大致如下所示:

let g = window; // outer global

let r = new Realm(); // realm object

 

let f = r.evaluate("(function() { return 17 })");

 

f() === 17 // true

 

Reflect.getPrototypeOf(f) === g.Function.prototype // false

Reflect.getPrototypeOf(f) === r.global.Function.prototype // true

這種技術實際上可以使用現有的JavaScript特性來實現,儘管這些特性鮮為人知。沙箱的一項任務就是隱藏全局變量。這個shim庫的核心功能大致如下所示:

function simplifiedEval(scopeProxy, userCode) {

 'use strict'

 with (scopeProxy) {

    eval(userCode)

  }

}

這是用於演示目的的簡化版本;真實版本中還是有一些細微差別的。但是,它展示了其中最關鍵的部分:with語句和Proxy對象。

其中,with(obj)語句創建了一個作用域,在該作用域內可以使用obj的屬性查找變量。在這個例子中,我們可以將變量PI、cos和sin解析為Math對象的屬性。另一方面,console並不是Math的屬性,因此需要在全局作用域內進行解析。

with (Math) {

  a = PI * r * r

  x = r * cos(PI)

 y = r * sin(PI)

 console.log(x,  y)

}

代理對象是JavaScript對象最動態的一種形式。

· 最基本的JavaScript對象可以通過訪問obj.x返回屬性的值。

· 更高級的JavaScript對象可以具有getter屬性,用於返回函數的計算結果。實際上,訪問obj.x就是調用x的getter屬性。

· 代理可以通過運行函數get來訪問任意屬性。

對於下面的代理(由於它僅用於演示,所以進行了相應的簡化處理)來說,當我們嘗試訪問它的任何屬性時,都將返回undefined,而不是對象whitelist中的屬性值。

const scopeProxy = new Proxy(whitelist, {

  get(target, prop) {

    // here, target === whitelist

    if (prop in target) {

      return target[prop]

    }

    return undefined

  }

}

現在,當您將這個代理用作with對象的參數時,它將攔截所有變量的解析過程,並且永遠不會使用全局作用域來解析變量:

with (proxy) {

  document // undefined!

 eval("xhr") // undefined!

}

不過,這種方法仍然可以通過諸如({}).constructor之類的表達式來訪問某些全局變量。此外,沙箱也確實需要訪問一些全局變量。例如,Object是一個全局對象,並且許多合法的JavaScript代碼(例如Object.keys)都需要用到它。

為了讓插件既能夠訪問這些全局變量又不會捅婁子,Realms沙箱支持通過創建同源的iframe來實例化所有這些全局變量的新副本。當然,這個iframe不會像在嘗試#1中那樣用作沙箱。並且,同源iframe不會受CORS的限制。

相反,當在與父文檔同源的情況下創建<inline-iframe>時:

1.它附帶了所有全局變量的單獨副本,例如Object.prototype等。

2.可以從父文檔訪問這些全局變量。

這些全局變量將被放進代理對象的「白名單」中,這樣的話,插件就可以訪問它們了。最後,這個新的<inline-iframe>還附帶了一個新的「eval」函數副本,它與現有的函數有一個重要的區別:即使只有通過({}).constructor這樣的語法才能訪問的內置值,也將會解析為iframe的副本。

這種基於Realms的沙箱方法有許多優秀的屬性:

它在主線程上運行。

速度很快,因為它可以使用瀏覽器的JavaScript JIT來執行代碼。

瀏覽器開發工具仍可以正常使用。

即使如此,我們還面臨令一個非常重要的問題:這種方法安全嗎?

(未完待續)

相關焦點

  • javascript成神之路(1):如何編寫高質量的js代碼
    原型以及原型鏈一、如何書寫可維護性的代碼當出現bug的時候如果你能立馬修復它是最好的,此時解決問題的四路在你腦中還是很清晰的。web頁面包含不是該頁面開發者所寫的代碼也是比較常見的,例如:第三方的JavaScript庫廣告方的腳本代碼第三方用戶跟蹤和分析腳本代碼不同類型的小組件,標誌和按鈕例如說,該第三方腳本定義了一個全局變量,叫做A;接著,在你的函數中也定義一個名為A的全局變量。其結果就是後面的變量覆蓋前面的,第三方腳本就一下子失效啦!
  • JavaScript 中 Eval 函數的前世今生,執行代碼字符串
    代碼壓縮工具(在把 JS 投入生產環境前對其進行壓縮的工具)將局部變量重命名為更短的變量(例如 a 和 b 等),以使代碼體積更小。這通常是安全的,但在使用了 eval 的情況下就不一樣了,因為局部變量可能會被 eval 中的代碼訪問到。因此壓縮工具不會對所有可能會被從 eval 中訪問的變量進行重命名。這樣會導致代碼壓縮率降低。
  • 聯想再爆安全漏洞,官方稱代碼為第三方公司提供
    7月5日消息,據外媒消息,聯想電腦存在安全漏洞,據安全研究院的Dymtro Oleksiuk稱,他發現黑客可以繞過Windows的基本安全協議對聯想電腦進行攻擊。問題出在系統維護模式(SMM)的原始碼中,這一般是主板驅動程序的一部分。
  • 0基礎學習JavaScript一定要知道如何使用VS2019去編寫代碼
    第1節. 編寫JS代碼對於.NET開發者來說,前端技術也是不能少的,0基礎的開發者來說,使用Visual Studio 2019開發工具是非常容易上手的。這裡創建一個空白的Web應用程式項目,ASP.NET Web應用程式是完全支持JavaScript代碼編寫的。
  • 翻譯 | 《JavaScript Everywhere》第18章 帶Electron的桌面應用程式(^_^)
    :*☆哎喲不錯哦第18章 帶Electron的桌面應用程式我第一次接觸個人計算機是在一個充滿Apple II機器的學校實驗室中進行的。每周一次,我和我的同學被老師帶進教室,給了一些軟盤,並給出了有關如何加載應用程式(通常是Oregon Trail)的粗略說明。
  • 讓C代碼在瀏覽器中運行——JavaScript慘遭拋棄?
    這意味著很快,就能在所有流行的瀏覽器中運行wasm了。在這篇文章中,我們將會演示如何將簡單的C代碼編譯為wasm,並將其包含在網頁中。在此之前,我們先來直觀的了解下WebAssembly是如何工作的。WebAssembly是如何工作的?這裡不涉及過多技術性的問題。
  • 如何在javascript中創建一個對象?
    javascript是一門基於對象而不是面向對象的語言,由於它的這個缺陷,在javascript中實現面向對象時十分彆扭,就比如創建對象,由於在ES6之前沒有class關鍵字,想要創建對象必須依賴以下幾種間接方式。
  • 在JavaScript中,使用replace()、test()和exec()方法匹配字符串
    JavaScript核心代碼如下:<script type="text/javascript">var str = "Hello Microsoft!"第2個參數是最終要替換為的新字符串。該方法的功能是將第1個參數匹配到的值替換為第2個參數的值。返回的結果就是替換後的新字符串。
  • JavaScript引擎實現JVM 支持運行Java
    一般的高級語言如果要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入JVM後,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用JVM屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。JVM在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。
  • 群暉NAS中通過Docker運行odoo並安裝第三方模塊
    首先在群暉NAS中安裝Docker,並在註冊表中下載odoo和postgres,詳情見:在群暉NAS上安裝Docker並運行Odoo等待odoo和postgres下載完後,先不要啟動。使用工具:MobaXterm然後輸入sudo -i回車輸入admin密碼現在可以看到前面的用戶變成了root啟動PostgreSQL伺服器直接複製下面代碼複製到
  • Javascript 生成器
    > 如果運行這段代碼,則會得到以下輸出: 下面我來解釋該程序是如何工作的。 生成器函數 首先,代碼中存在生成器函數的定義: 後面的 告訴 javascript 這是一個生成器函數。以下寫法都是生成器函數的有效定義。 並不是函數名的一部分。而是 符號定義了生成器。
  • JavaScript中使用bind()方法讓代碼更乾淨
    翻譯作者:碼農網 – 小峰幾個星期前,我寫了一篇關於如何在一個循環中關閉調用異步函數的影響以及處理這個問題的若干方法。在我最近的編碼中,我發現了一個更簡單的方法。在這個過程中,它消除了匿名函數並刪除了linting錯誤,「不要在循環中寫函數」。你看,我一直在使用JavaScript中的bind()試驗。事實證明,我們可以在多個場合,包括處理我幾周前提到的閉包問題中,使用bind。什麼是bind()?
  • 七天學會javascript第一天javascript介紹
    javascript :客戶端編程。javascript是由客戶端去解釋運行的。怎麼引入javascript 呢?js代碼ps:js代碼嚴格區分大小寫,每行代碼以分號;結尾.js運算符介紹代碼運行結果js獲取時間下圖是運行js代碼結果
  • JavaScript代碼風格要素
    add2也沒有列出一系列的參數,因為該函數不在其內部處理一系列的參數,相反,它返回了一個知道如何處理參數的新函數。函數組合是將一個函數的輸出作為另一函數的輸入的過程。 也許你沒有意識到,你一直在使用函數組合。鏈式調用的代碼基本都是這個模式,比如數組操作時使用的.map(),Promise 操作時的promise.then()。
  • JavaScript支持計時事件,如何實現超時調用和間隔調用?
    其實這個功能只需要使用JavaScript中的間隔調用就可以實現,另外,還有超時調用,在設定一段時間之後,就會執行某個函數或某段代碼。能夠在指定時間後執行代碼或重複執行代碼的函數,我們稱為計時事件,在JavaScript中,有2個計時事件,分別為:(1). 超時調用:使用setTimeout()方法表示。(2).
  • JavaScript 執行機制
    1.關於javascriptjavascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變。所以一切javascript版的"多線程"都是用單線程模擬出來的,一切javascript多線程都是紙老虎!
  • JavaScript是什麼
    面向過程的思想----C語言【指針】面向對象的思想就是只需要知道開始和結束位置就可以,至於中間是如何完成的不需要關注。開始和結束位置就是對象。面向對象的思想—java、 C#腳本—寫好的程序不需要中間轉換,就能立即在運行環境中運行。javaScript,SQL為html網頁提供動態效果【特效】。
  • javascript流程語句
    共有三種基本結構:順序、分支、循環順序:從上朝下執行的代碼就是順序選擇:根據不同的情況,執行對應代碼循環:重複做一件事情順序結構 順序結構是最簡單的程序結構,它是由若干個依次執行的處理步驟組成的。例如,A語句和B語句是依次執行的,只有在執行完A語句後,才能接著執行B語句。
  • 【第590期】解開麵條代碼: 怎樣書寫可維護JavaScript
    單元測試是用來測試功能和代碼的方法是否按預期調用的一種方式。相比閱讀代碼和運行代碼,單元測試能更深入的幫你了解代碼。如果在你的項目中還沒有單元測試,別急,我們接著往下看。二、創建一個基準這些都是關於代碼一致性的內容。現在你已經了解了項目中使用的所有工具集,你知道了代碼的結構和邏輯功能的位置,是時候建立一個基準了。
  • 一行代碼證明編程能力,javascript程式語言中,經典語句精髓解析
    javascript程式語言中,經典語句精髓解析,一行代碼證明編程能力!程式設計師:十萬行代碼,證明編程基礎的掌握;之後,一行代碼證明編程的能力!1、if語句在javascript語言中,if條件語句是很常用到的。與其他程式語言相比,還是有差異的。