Categories
程式開發

怎樣解決JavaScript生態中第三方安全性問題?


本文最初發佈於Medium網站,經原作者授權由InfoQ中文站翻譯並分享。

我最近發了一條推文,談到JavaScript生態系統中第三方安全性問題的現狀:

怎樣解決JavaScript生態中第三方安全性問題? 1

我想補充一些背景資料,談一談自己對Node.js模塊和安全性概念的研究,以及Agoric SES和隔離模型(compartment model),還有Node.js、Deno和瀏覽器運行時對生態系統所需的第三方安全性的支持缺位。

太長不看版:我認為我們需要考慮為JS引入新的、更安全的運行時,這需要付出一系列的努力,包括模塊化的組件、將隔離的作用域添加到導入映射,以及對現有生態系統保持兼容的謹慎的安全模型。

更新:本文發布後,我看到遠藤熔岩護城河提供了非常接近這些方向的技術,不過它們都沒有像我期望的那樣激進——我認為有必要將這樣的安全系統集成到主要運行時的自身中。

第三方安全性問題

這裡的本質問題是npm install。 隨著npm生態不斷擴大,我們對它的依賴越來越深,與此同時,我們每天運行的不受信任代碼也越來越多,這方面的安全漏洞日漸嚴重。

那些兼職維護者現在發現自己需要不斷回應常規的安全問題,否則,它們的軟件包可能就會被貼上無法修補漏洞的警示,這些安全問題可能是,也可能都不是真正的權限提升漏洞。 我們為自己營造了一種安全環境的幻象,而實際上所有東西都太不安全了。

許多公司並沒有坐視不理,而是積極努力緩解這些安全問題。 不過,麻煩在於,他們的做法最終會產生一個個生態系統的分支,或者是現有生態系統的補丁,但這些安全措施從來沒有從根本上融入生態系統本身。 第三方安全性依舊是個難以處理的大問題,這只有專業團隊才能應付,例如菲格瑪銷售隊伍的這些例子。

Realms提案可能為我們提供了用於構建安全運行時的工具,但JavaScript生態系統約定本身就是抵觸安全限制的。

Chrome/v8的一般觀點是,這種類型的同進程內安全性措施無法應用在所有第三方包上:

怎樣解決JavaScript生態中第三方安全性問題? 2

現在,我承認自己完全認可了OCAP、SES和隔離模型的優雅設計,Agoric的那些人(他們是TC39的長期成員)也是這麼看的。 我在Node.js協作峰會上就這些概念做了演講。

模塊化安全性概念有很多非常突出的優勢,當然,它也有一些明顯缺陷,但我認為我們應該積極解決這些問題並完成這一工作。 除非有人能完全證明它是不可行的,否則我們不該放棄同進程的模塊化安全性模型。

隔離模型

隔離模型的主旨基於SES(安全ECMAScript),如Agoric所言,其內容大概是這樣的:

  1. 所有功能(capability)都是通過模塊系統導入的(import fetch from 'fetch'之類)——模塊解析器充當功能係統,強制執行權限。
  2. 因為(1),我們應該禁用/謹慎控制所有全局功能。
  3. JavaScript需要大量修補工作,以防止原型突變和意外的側通道(side-channel),例如return{toString(){}}對象hooks。 你必須非常謹慎地管理程序包接口,並凍結原型突變中的整個全局對象。

請參考Mark Miller關於“極端模塊化分佈式JavaScript”的演講,或者我在Node.js協作峰會上所做的“安全性、模塊和Node.js”演講,來更深入地了解整個模型。

從理論上講,這個模型能夠限制破壞性代碼。 你通過npm安裝的日期時間庫無法在你的計算機上安裝特洛伊木馬,這似乎是非常有用的屬性。

關於(3),我們在Node.js中發布了–frozen-intrinsics標誌。 (1)和(2)顯然要求對當今所有的運行時進行重大更改。

批評

對這種模型的批評包括Spectre類漏洞,提供安全跨包接口的困難,還有人說這些想法在理論上聽起來不錯,但在真實的JS環境中不切實際。

幽靈

Spectre類攻擊說的是在進程上運行的代碼,可以使用CPU逆向工程和時間信息來讀取同一進程中其他獨立代碼使用的秘密信息,比如密碼、安全令牌等

首先要注意的是,Spectre是擁有竊取秘密信息的​​能力,而不是具備在計算機上安裝木馬的能力。 即使我們不能通過新模型完全避免Spectre(我們當然可以這樣嘗試),但我們仍在限制破壞性功能(例如向互聯網上的隨機人員提供完整的磁盤和網絡訪問權限),這就是一個巨大勝利。 我們這種模型所對比的是根本沒有針對第三方庫的單獨安全性的情況,今天的Node.js、Deno和瀏覽器就是這種情況。 在遭受攻擊的情況下,最好只丟掉一張信用卡,而不是丟掉一張信用卡,然後把房子燒掉。

這裡要注意的第二件事是,如果你擁有一個真正的功能係統並且可以謹慎地控製網絡訪問,那麼滲透功能(基本上是使用fetch)本身就可以視為關鍵權限。 秘密可能會被發現,但不那麼容易被共享。

但控制滲透的能力也沒那麼容易實現,人們總會發現側邊通道——閃爍的光線會穿過那些無論有多複雜的窗口,並共享秘密令牌的信息。 這是一條需要應對的複雜邊界。

最後,就真正的Spectre對抗措施而言,Cloudflare的Cloudflare Workers的同進程部署也存在相同的問題,他們最近在這裡討論了這個話題——緩解Spectre和其他安全威脅:Cloudflare Workers安全模型

他們的應對措施總結如下:

  • 通過新的Worker(允許自定義計時器創建)限制Date.now()和多線程,以禁用發起攻擊所需的時間度量。
  • 基於監視和完全隔離來主動檢測攻擊行為。
  • 探索內存隨機化技術,以使秘密信息不會保持靜態。

正如Cloudflare所提到的,這是一個可以持續發展的主動應對方法類型。 從理論上講,這類緩解措施也能應用於新的運行時。

需要注意的一個重點是,這些緩解技術完全無法應用於Web平台,因為它們根本做不到(至少在沒有Realms的情況下)。 從這個角度來看,Google/v8的觀點的確沒錯,但是我要重點關注的是新的JavaScript運行時,例如Node.js的後繼者(如Deno等),它們今天確實應該探索這些安全屬性。

不安全的模塊接口

下一個主要問題歸結於第三方軟件包之間的複雜接口邊界。 例如,考慮以下代碼:

import { renderer } from 'renderer';
import { renderGraph } from 'graph';
import { renderTitle } from 'title';

renderer.render([renderGraph, renderTitle]);

從理論上講,renderGraph不需要調用renderer的其他任何功能,因此可以將其視為低信任度代碼。
但現在考慮一下renderGraph的惡意實現:

export function renderGraph () {
  this[1].setTitle('Changed the title');
}

renderGraph知道renderer將通過renderArray[i]()調用它,在JavaScript中它將此綁定設置為數組本身,從而允許從graph組件訪問title組件。

是的,這是一個人為的示例,但是它演示瞭如何在JavaScript中輕鬆實現功能洩漏,而這甚至還沒有涉及到信息洩漏(例如通過toString())。

鎖住這些無意間造成的側通道,意味著要讓所有程序包接口都接入沒有這些可怕缺陷的SafeFunctionSafeObject對象,這不是一個容易解決的問題——需要付出大量努力。

另一方面,Web Assembly模塊接口沒有JavaScript中的這類功能和信息洩漏,這無疑為處理這些問題的未來生態系統帶來了希望。

不切實際的約束

第三個論點是,這種安全性要求對JavaScript及其生態系統的限制太多了。 當今的生態系統是不可能發展為這種安全的生態系統的。 結果,安全的運行時永遠都會是少數人選擇的附加屬性,他們可以投入大量的時間和精力來支持它們。

我認為這是最關鍵的問題。 運行低風險第三方庫的能力應完全民主化。

Secure Modular Runtime(安全模塊化運行時)提案

我想提出一個JavaScript的運行時假設,請大家仔細檢察它能否解決以下問題:

  1. 該運行時可以完全限制來自第三方程序包的高級功能訪問,這些第三方程序運行在Node.js、Deno和瀏覽器的相同進程上。
  2. 該運行時可以支持現有JavaScript生態系統的升級,這對於推廣採用是至關重要的。

該提案基於一個安全的運行時,因為從一開始就設計安全性的話就會是這樣的結論。 JavaScript生態系統是受運行時主導的,只有提供安全的運行時目標,我們才能開始將生態系統塑造為更安全的形態。

這種運行時的形式是SES隔離模型的直接實現:

  • 全局對像不應具有任何功能(沒有fetchWorkerDate全局變量),只有內聯函數,而所有這些內聯函數均應作為安全的內聯實例提供。 所有功能都是導入的。
  • 權限模型應使用導入映射,並使用隔離的作用域實現,其中,作用域完全沒有回退,並且程序包不能導入超出其作用域的任何內容,除非在映射中明確定義。 這會將導入映射視為解析和每個包功能權限的唯一信源,並由作用域映射實現。
  • 所有包之間的接口都應使用SafeObjectSafeFunctionSafeClass實現——這是精心挑選的用於通信的語言子集,模塊系統本身要確保程序包遵守它們。 這可以是動態的包裝和展開,也可以是更靜態的,甚至是用戶定義的。
  • 應通過可在這個新的安全模型內運行至少90%的現有代碼的codemod,來支持現有的npm生態系統。

孤立的範圍

Isolated Scopes(隔離作用域)提案是Import Maps(導入映射)的擴展提案,允許導入映射全面定義可以導入和不能導入的內容。

該提案是基於Node.js策略和導入映射最終趨同的想法而產生的。 在SystemJS中,我們需要導入映射來支持完整性;而在Node.js中,我們需要策略(Policy)來支持導入映射樣式的作用域和映射。

顯而易見,技術上的一致性完全是自然產生的,但這指出了一條路徑:導入映射是定義功能完整性的自然之所。 如果我們可以在此處合併目標,就能解決“亡羊補牢”問題,因為構建導入映射的用戶並不關心安全性,而是將其作為工作流本身的副作用(如果他們選擇啟用強大的功能約束)。

這個想法是,在功能模型中,你最終將定義這樣的權限:

{
  "packageA": {
    "capabilities": ["packageB"]
  },
  "packageB": {
    "capabilities": "fs?local"
  }
}

除非通過功能係統明確授予訪問權限,否則,程序包無法導入包外的任何內容。

這個應用程序的導入映射類似這樣:

{
  "imports": {
    "packageA": "/path/to/packageA/main.js"
  },
  "scopes": {
    "/path/to/packageA/": {
      "packageB": "/path/to/packageB/main.js"
    },
    "/path/to/packageB/": {
      "fs": "core:fs?local"
    }
  }
}

功能信息已經在導入映射中自然定義了,也就是說它是冗餘信息。 同樣,另一方面,Node.js策略看起來很像導入映射

為支持此操作而對導入映射所做的更改是很小的,可以作為擴展提案來完成:

  1. 為導入映射提供新的"isolatedScope": true選項,由頂級屬性、標誌或其他方式啟用。
  2. 將作用域限制為不允許該作用域之外的URL導入,除非該URL在映射中明確定義。
  3. 禁止作用域回退。

通過這些小的調整,我們就有可能將導入映射轉換為用於應用程序開發的主要模塊化工作流,這種工作流易於審核、閱讀和管理,並且從一開始就內置了功能定義。

包接口

在包接口方面,導出的包綁定(例如,Node.js“main”/“exports”字段模塊導出)將使用安全接口系統。

我們將現有包的外向組件轉換為這種安全形式,例如:

export function renderGraph () {
  this[1].setTitle('Changed the title');
}

將轉換為在運行時中執行:

export function SafeFunction(renderGraph () {
  this[1].setTitle('Changed the title');
})

SafeFunction實現將確保調用者不對this進行重新綁定。 因此,所有功能參考對於軟件創建者都是完全明確的。 Advisory仍然是必需的,但它們現在位於一個定義明確且受約束的權限模型中,該模型明確定義了權限提升的真正含義。

SafeObject遞歸應用,而SafeFunction則在運行時動態地將相同的清除方法應用於其返回值。 實時導出綁定賦值可以用SafeValue基類重新賦值操作代替。 原語保持不變。

可以採用多種方法來應用這些安全函數:

  1. 明確要求用戶使用這些接口,例如FnObjCls - export Fn(() => {})等全局名稱,作為Agoric的harden的變體。
  2. 這些安全包裝器可以作為運行時模塊包裝器,完全不需要用戶的任何干預。
  3. 某種預編譯階段可以自動注入安全接口。
  4. 引擎的工作可以使這些一流的原語成為可能,並且理論上新的運行時可以逐漸在上游發展自己的方向。

上面的內容在打包時的性能開銷方面有所不同,但仔細考慮用例的話,應該可以優化必要的性能屬性,同時保持這些安全性保證。

這是模型最關鍵的部分,這裡可能要解決一些非常複雜的情況,但是我還沒有聽過實現這些定義明確的接口場景有什麼重大障礙。

生態系統兼容性

可以使用codemods提供現有的JavaScript支持,它們將包轉換為要在安全運行時中執行的形式。 這並非易事,但在超過90%的情況下應該能提供生態系統兼容性。 例如:

export async function getCurrentResource () {
  return fetch(`${globalThis.resourceUrl}/${Date.now()}`);
}

可以轉換為:

import fetch from 'fetch';
import { now } from 'date';
export Fn(async getCurrentResource () => {
  return fetch(`${import.meta.local.resourceUrl}/${now()}`);
});

其中,fetchdate是受控功能權限,而import.meta.local代表程序包級別的全局變量,可以在應用程序級別設置該包以支持未知的全局訪問情況。

這樣,我們可以將來自npm的現有第三方程序包完全編碼為安全包約定。

如果這聽起來門檻太高了,請記住,我們現在每次使用構建工具鏈時,就已經對所有npm代碼做了codemod,而這些技術正是jspm支持瀏覽器導入所用的。

小結

只要JavaScript還有採用模塊化安全性的希望,我們就應該為此努力,因為這似乎是我們未來安全地運行第三方代碼的最佳選擇。 Node.js、Deno或瀏覽器是做不到的,因為它們各自的產品都不支持本文描述的所需屬性——它們的內在約定還是抵觸限制第三方包功能的做法。

如果我們確實發現安全包接口對於JavaScript而言確實不可行,那麼就應該將這些想法移入Wasm,並確保我們可以為以後的Wasm運行時獲取這些屬性。

但是,請不要拋棄在JavaScript上解決這些安全問題的潛力,即使這裡還存在不確定性。 因為除非現在我們積極努力進化出安全的JavaScript生態系統,否則,我們只會在以後亡羊補牢。

原文鏈接:

https://guybedford.com/secure-modular-runtimes.html