構建一個安全的 JavaScript 沙箱

發表於2016-08-23

6c0378f8gw1f730uldkfrj20p00dwaf1

在 Node.js 中有一個模組叫做 VM,它提供了幾個 API,允許程式碼在 V8 虛擬機器上下文中執行,如:

vm.Script 中的程式碼是預編譯好的,通過 vm.createContext 將程式碼載入到一個上下文環境中,置入沙箱(sandbox),然後通過 script.runInContext 執行程式碼,整個操作都在封閉的 VM 中進行。這是 Node.js 提供給我們的便捷功能,那麼,在瀏覽器環境中呢?是否也能做到將程式碼執行在沙箱中?本文帶著大家來探索一番。

邪惡的 eval

eval 函式可以將一個 Javascript 字串視作程式碼片段執行,不過它存在諸多問題,如除錯困難、效能問題等,並且它在執行時可以訪問閉包環境和全域性作用域,存在程式碼注入的安全風險,作為沙箱,這也是我們不期望看到的。eval 雖然好用,但是經常被濫用,在這裡我們不多討論它。

new Function

Function 建構函式會建立一個新的函式物件,它可以作為 eval 的替代品:

返回的 fn 是一個定義好的函式,最後一個引數為函式體。它和 eval 不太一樣:

  • fn 是一段編譯好的程式碼,可以直接執行,而 eval 需要編譯一次
  • fn 沒有對所在閉包的作用域訪問許可權,不過它依然能夠訪問全域性作用域

如何阻止它訪問全域性作用域呢?

with 是阻止程式訪問上一級作用域的一道防火牆:

如上程式碼,code 被執行時,首先會尋找 sandbox 中的變數,如果不存在,會往上追溯global 物件,雖然有一道防火牆,但是依然不能阻止 fn 訪問全域性作用域。

似乎在 ECMAScript 5 中掌握的知識已經不足以解決 code 逃逸沙箱的問題了,此時我們可以把焦點放在 ES6 提供的新特性上。

ES6 中提供了一個 Proxy 函式,它是訪問物件前的一個攔截器,下面舉一個簡單的栗子:

程式碼中,Proxy{} 設定了屬性訪問攔截器,倘若訪問的屬性為 a 則返回 1,否則走正常程式。

這裡我們可以使用 proxy 對訪問做攔截處理,sandbox 本不存在的屬性會追溯到全域性變數上訪問,此時我們可以欺騙程式,告訴它這個「不存在的屬性」是存在的,於是有了下面的程式碼:

似乎這麼做就可以了,但既然用到了 ES6 的特性,我們便不能忽略 ES6 中一個可以控制with 關鍵詞行為的變數。

Symbol 是 JS 的第七種資料型別,它能夠產生一個唯一的值,同時也具備一些內建屬性,這些屬性可以用來進行超程式設計(meta programming),即對語言本身程式設計,影響語言行為。其中一個內建屬性 Symbol.unscopables,通過它可以影響 with 的行為。

上面對 A 設定做了 Symbol.unscopables 的設定,宣告 foo 屬性在 A 上是不存在的,從而使得程式碼從 with 中逃逸。對此,我們需要對它做一層加固:

不過,這裡還存在兩個邏輯漏洞:

  • code 中可以提前關閉 sandboxwith 語境,如 '} alert(this); {'
  • code 中可以使用 evalnew Function 直接逃逸

對於第一個問題,我們可以通過堆疊深度檢測:

事實上,這樣做依然不嚴謹,比如程式碼註釋中出現花括號問題,如 /*{*/'} alert(this); {'/*}*/;而對於第二個問題,暫時還沒有什麼好的辦法,尤其是 Function,它可以通過很多方式構造出來:

靈活是 Javascript 這門語言的特性,也是它難以被掌控的主要原因,這點可以從文中各種沙箱逃逸方式就能看出。ES6 提供了很多新的特性,本文以沙箱為切入點,帶著大家學習了幾個函式和屬性,希望讀者有些收穫。

本文沒有得到一個完美的答案,但是這個問題依然值得思考和研究。

有一個比較不錯的思路是,通過 iframe 執行程式碼,執行的結果通過 postMessage 函式通訊傳輸給操作者。並且 iframe 還提供了很多可供設定的安全引數,如 allow-scripts,allow-forms, allow-same-origin, allow-top-navigation 等等,方便我們對沙箱做安全控制。

相關文章