| 導語 其實在前端編碼中,或多或少都會接觸到沙箱,可能天真善良的你沒有留意到,又可能,你還並不知道它的真正用途,學會使用沙箱,可以避免潛在的代碼注入以及未知的安全問題。
前言
沙箱,即sandbox,顧名思義,就是讓你的程序跑在一個隔離的環境下,不對外界的其他程序造成影響,通過創建類似沙盒的獨立作業環境,在其內部運行的程序並不能對硬盤產生永久性的影響。
舉個簡單的栗子,其實我們的瀏覽器,Chrome 中的每一個標簽頁都是一個沙箱(sandbox)。渲染進程被沙箱(Sandbox)隔離,網頁 web 代碼內容必須通過 IPC 通道才能與瀏覽器內核進程通信,通信過程會進行安全的檢查。沙箱設計的目的是為了讓不可信的代碼運行在一定的環境中,從而限制這些代碼訪問隔離區之外的資源。
JS中沙箱的使用場景
前端JS中也會有應用到沙箱的時候,畢竟有時候你要獲取到的是第三方的JS文件或數據?而這數據又是不一定可信的時候,創建沙箱,做好保險工作尤為重要。
1、jsonp:解析服務器所返回的jsonp請求時,如果不信任jsonp中的數據,可以通過創建沙箱的方式來解析獲取數據;(TSW中處理jsonp請求時,創建沙箱來處理和解析數據);
2、執行第三方js:當你有必要執行第三方js的時候,而這份js文件又不一定可信的時候;
3、在線代碼編輯器:相信大家都有使用過一些在線代碼編輯器,而這些代碼的執行,基本都會放置在沙箱中,防止對頁面本身造成影響;(例如:https://codesandbox.io/s/new)
4、vue的服務端渲染:vue的服務端渲染實現中,通過創建沙箱執行前端的bundle文件;在調用createBundleRenderer方法時候,允許配置runInNewContext為true或false的形式,判斷是否傳入一個新創建的sandbox對象以供vm使用;
5、vue模板中表達式計算:vue模板中表達式的計算被放在沙盒中,只能訪問全局變量的一個白名單,如 Math 和 Date 。你不能夠在模板表達式中試圖訪問用戶定義的全局變量。
總而言之:當你要解析或執行不可信的JS的時候,當你要隔離被執行代碼的執行環境的時候,當你要對執行代碼中可訪問對象進行限制的時候,沙箱就派上用場了。
沙箱實現一:with + new Function
首先從最簡陋的方法說起,假如你想要通過eval和function直接執行一段代碼,這是不現實的,因為代碼內部可以沿着作用域鏈往上找,篡改全局變量,這是我們不希望的,所以你需要讓沙箱內的變量訪問都在你的監控范圍內;不過,你可以使用with API,在with的塊級作用域下,變量訪問會優先查找你傳入的參數對象,之后再往上找,所以相當於你變相監控到了代碼中的“變量訪問”:
接下里你要做的是,就是暴露可以被訪問的變量exposeObj
,以及阻斷沙箱內的對外訪問。通過es6提供的proxy特性,可以獲取到對對象上的所有改寫:
通過設置has函數,可以監聽到變量的訪問,在上述代碼中,僅暴露個別外部變量供代碼訪問,其余不存在的屬性,都會直接拋出error。其實還存在get、set函數,但是如果get和set函數只能攔截到當前對象屬性的操作,對外部變量屬性的讀寫操作無法監聽到,所以只能使用has函數了。接下來我們測試一下:
看起來一切似乎沒有什么問題,但是問題出在了傳入的對象,當調用的是console.log(a.b)的時候,has方法是無法監聽到對b屬性的訪問的,假設所執行的代碼是不可信的,這時候,它只需要通過a.b.__proto__就可以訪問到Object構造函數的原型對象,再對原型對象進行一些篡改,例如將toString就能影響到外部的代碼邏輯的。
例如上面所展示的代碼,通過訪問原型鏈的方式,實現了沙箱逃逸,並且篡改了原型鏈上的toString方法,一旦外部的代碼執行了toString方法,就可以實現xss攻擊,注入第三方代碼;由於在內部定義執行的函數代碼邏輯,仍然會沿着作用於鏈查找,為了繞開作用域鏈的查找,筆者通過訪問箭頭函數的constructor的方式拿到了構造函數Function,這個時候,Funtion內所執行的xss代碼,在執行的時候,便不會再沿着作用域鏈往上找,而是直接在全局作用域下執行,通過這樣的方式,實現了沙箱逃逸以及xss攻擊。
你可能會想,如果我切斷原型鏈的訪問,是否就杜絕了呢?的確,你可以通過Object.create(null)的方式,傳入一個不含有原型鏈的對象,並且讓暴露的對象只有一層,不傳入嵌套的對象,但是,即使是基本類型值,數字或字符串,同樣也可以通過__proto__查找到原型鏈,而且,即使不傳入對象,你還可以通過下面這種方式繞過:
可見,new Function + with的這種沙箱方式,防君子不防小人,當然,你也可以通過對傳入的code代碼做代碼分析或過濾?假如傳入的代碼不是按照的規定的數據格式(例如json),就直接拋出錯誤,阻止惡意代碼注入,但這始終不是一種安全的做法。
沙箱實現二:借助iframe實現沙箱
前面介紹一種劣質的、不怎么安全的方法構造了一個簡單的沙箱,但是在前端最常見的方法,還是利用iframe來構造一個沙箱,such as 在線代碼編輯器中:https://codesandbox.io/s/news。
這種方式更為方便、簡單、安全,也是目前比較通用的前端實現沙箱的方案,假如你要執行的代碼不是自己寫的代碼,不是可信的數據源,那么務必要使用iframe沙箱。sandbox是h5的提出的一個新屬性, 啟用方式就是在iframe標簽中使用sandbox屬性:
但是這也會帶來一些限制:
1. script腳本不能執行
2. 不能發送ajax請求
3. 不能使用本地存儲,即localStorage,cookie等
4. 不能創建新的彈窗和window
5. 不能發送表單
6. 不能加載額外插件比如flash等
不過別方,你可以對這個iframe標簽進行一些配置:
接下里你只需要結合postMessage API,將你需要執行的代碼,和需要暴露的數據傳遞過去,然后和你的iframe頁面通信就行了。
1)不過你需要注意的是,在子頁面中,要注意不要讓執行代碼訪問到contentWindow對象,因為你需要調用contentWindow的postMessageAPI給父頁面傳遞信息,假如惡意代碼也獲取到了contentWindow對象,相當於就拿到了父頁面的控制權了,這個時候可大事不妙。
2)當你使用postMessageAPI的時候,由於sandbox的origin默認為null,需要設置allow-same-origin允許兩個頁面進行通信,意味着子頁面內可以發起請求,這時候你需要防范好CSRF,允許了同域請求,不過好在,並沒有攜帶上cookie。
3)當你調用postMessageAPI傳遞數據給子頁面的時候,傳輸的數據對象本身已經通過結構化克隆算法復制,如果你還不了解結構化克隆算法可以查看這個。
簡單的說,通過postMessageAPI傳遞的對象,已經由瀏覽器處理過了,原型鏈已經被切斷,同時,傳過去的對象也是復制好了的,占用的是不同的內存空間,兩者互不影響,所以你不需要擔心出現第一種沙箱做法中出現的問題。
nodejs中的沙箱使用
nodejs中使用沙箱很簡單,只需要利用原生的vm模塊,便可以快速創建沙箱,同時指定上下文。
vm中提供了runInNewContext、runInThisContext、runInContext三個方法,三者的用法有個別出入,比較常用的是runInNewContext和runInContext,可以傳入參數指定好上下文對象。
但是vm是絕對安全的嗎?不一定。
通過上面這段代碼,我們可以通過vm,停止掉主進程nodejs,導致程序不能繼續往下執行,這是我們不希望的,解決方案是綁定好context上下文對象,同時,為了避免通過原型鏈逃逸(nodejs中的對象並沒有像瀏覽器端一樣進行結構化復制,導致原型鏈依然保留),所以我們需要切斷原型鏈,同時對於傳入的暴露對象,只提供基本類型值。
讓我們來看一下TSW中是怎么使用的:
通過runInNewContext返回沙箱中的構造函數Function,同時傳入切斷原型鏈的空對象防止逃逸,之后再外部使用的時候,只需要調用返回的這個函數,和普通的new Function一樣調用即可。
即使這樣,我們也不能保證這是絕對的安全,畢竟可能還有潛在的沙箱漏洞呢?
總結
即使我們知道了如何在開發過程中使用沙箱來讓我們的執行環境不受影響,但是沙箱也不一定是絕對安全的,畢竟每年都有那么多黑客絞盡腦汁鑽研出如何逃出瀏覽器沙箱和nodejs沙箱,因此筆者個人建議:
1、業務代碼上不執行不可信任的第三方JS,如有必要執行第三方JS,可通過設置CSP維護白名單的方式;
2、不要信任任何用戶數據源,防止惡意用戶注入代碼。
出於好奇整理了這篇文章,如有錯誤還望斧正。
原作者:騰訊IVWEB團隊,原鏈接:https://juejin.im/post/5d8d76b8e51d45781332e928
來源:掘金