摘要:當(dāng)運(yùn)行函數(shù)的時(shí)候,只能訪問(wèn)自己的本地變量和全局變量,不能訪問(wèn)構(gòu)造器被調(diào)用生成的上下文的作用域。如何建立一個(gè)更安全一些的沙箱通過(guò)上文的探究,我們并沒(méi)有找到一個(gè)完美的方案在建立安全的隔離的沙箱。
有哪些動(dòng)態(tài)執(zhí)行腳本的場(chǎng)景?
在一些應(yīng)用中,我們希望給用戶提供插入自定義邏輯的能力,比如 Microsoft 的 Office 中的 VBA,比如一些游戲中的 lua 腳本,F(xiàn)ireFox 的「油猴腳本」,能夠讓用戶發(fā)在可控的范圍和權(quán)限內(nèi)發(fā)揮想象做一些好玩、有用的事情,擴(kuò)展了能力,滿足用戶的個(gè)性化需求。
大多數(shù)都是一些客戶端程序,在一些在線的系統(tǒng)和產(chǎn)品中也常常也有類似的需求,事實(shí)上,在線的應(yīng)用中也有不少提供了自定義腳本的能力,比如 Google Docs 中的 Apps Script,它可以讓你使用 JavaScript 做一些非常有用的事情,比如運(yùn)行代碼來(lái)響應(yīng)文檔打開事件或單元格更改事件,為公式制作自定義電子表格函數(shù)等等。
與運(yùn)行在「用戶電腦中」的客戶端應(yīng)用不同,用戶的自定義腳本通常只能影響用戶自已,而對(duì)于在線的應(yīng)用或服務(wù)來(lái)講,有一些情況就變得更為重要,比如「安全」,用戶的「自定義腳本」必須嚴(yán)格受到限制和隔離,即不能影響到宿主程序,也不能影響到其它用戶。
而 Safeify 就是一個(gè)針對(duì) Nodejs 應(yīng)用,用于安全執(zhí)行用戶自定義的非信任腳本的模塊。
怎樣安全的執(zhí)行動(dòng)態(tài)腳本?我們先看看通常都能如何在 JavaScript 程序中動(dòng)態(tài)執(zhí)行一段代碼?比如大名頂頂?shù)?eval
eval("1+2")
上述代碼沒(méi)有問(wèn)題順利執(zhí)行了,eval 是全局對(duì)象的一個(gè)函數(shù)屬性,執(zhí)行的代碼擁有著和應(yīng)用中其它正常代碼一樣的的權(quán)限,它能訪問(wèn)「執(zhí)行上下文」中的局部變量,也能訪問(wèn)所有「全局變量」,在這個(gè)場(chǎng)景下,它是一個(gè)非常危險(xiǎn)的函數(shù)。
再來(lái)看看 Functon,通過(guò) Function 構(gòu)造器,我們可以動(dòng)態(tài)的創(chuàng)建一個(gè)函數(shù),然后執(zhí)行它
const sum = new Function("m", "n", "return m + n"); console.log(sum(1, 2));
它也一樣的順利執(zhí)行了,使用 Function 構(gòu)造器生成的函數(shù),并不會(huì)在創(chuàng)建它的上下文中創(chuàng)建閉包,一般在全局作用域中被創(chuàng)建。當(dāng)運(yùn)行函數(shù)的時(shí)候,只能訪問(wèn)自己的本地變量和全局變量,不能訪問(wèn) Function 構(gòu)造器被調(diào)用生成的上下文的作用域。如同一個(gè)站在地上、一個(gè)站在一張薄薄的紙上一樣,在這個(gè)場(chǎng)景下,幾乎沒(méi)有高下之分。
結(jié)合 ES6 的新特性 Proxy 便能更安全一些
function evalute(code,sandbox) { sandbox = sandbox || Object.create(null); const fn = new Function("sandbox", `with(sandbox){return (${code})}`); const proxy = new Proxy(sandbox, { has(target, key) { // 讓動(dòng)態(tài)執(zhí)行的代碼認(rèn)為屬性已存在 return true; } }); return fn(proxy); } evalute("1+2") // 3 evalute("console.log(1)") // Cannot read property "log" of undefined
我們知道無(wú)論 eval 還是 function,執(zhí)行時(shí)都會(huì)把作用域一層一層向上查找,如果找不到會(huì)一直到 global,那么利用 Proxy 的原理就是,讓執(zhí)行了代碼在 sandobx 中找的到,以達(dá)到「防逃逸」的目的。
在瀏覽器中,還可以利用 iframe,創(chuàng)建一個(gè)再多安全一些的隔離環(huán)境,本文著眼于 Node.js,在這里不做過(guò)多討論。
在 Node.js 中呢,有沒(méi)有其它選擇
或許沒(méi)看到這兒之前你就已經(jīng)想到了 VM,它是 Node.js 默認(rèn)就提供的一個(gè)內(nèi)建模塊,VM 模塊提供了一系列 API 用于在 V8 虛擬機(jī)環(huán)境中編譯和運(yùn)行代碼。JavaScript 代碼可以被編譯并立即運(yùn)行,或編譯、保存然后再運(yùn)行。
const vm = require("vm"); const script = new vm.Script("m + n"); const sandbox = { m: 1, n: 2 }; const context = new vm.createContext(sandbox); script.runInContext(context);
執(zhí)行上這的代碼就能拿到結(jié)果 3,同時(shí),通過(guò) vm.Script 還能指定代碼執(zhí)行了「最大毫秒數(shù)」,超過(guò)指定的時(shí)長(zhǎng)將終止執(zhí)行并拋出一個(gè)異常
try { const script = new vm.Script("while(true){}",{ timeout: 50 }); .... } catch (err){ //打印超時(shí)的 log console.log(err.message); }
上面的腳本執(zhí)行將會(huì)失敗,被檢測(cè)到超時(shí)并拋出異常,然后被 Try Cache 捕獲到并打出 log,但同時(shí)需要注意的是 vm.Script 的 timeout 選項(xiàng)「只針對(duì)同步代有效」,而不包括是異步調(diào)用的時(shí)間,比如
const script = new vm.Script("setTimeout(()=>{},2000)",{ timeout: 50 }); ....
上述代碼,并不是會(huì)在 50ms 后拋出異常,因?yàn)?50ms 上邊的代碼同步執(zhí)行肯定完了,而 setTimeout 所用的時(shí)間并不算在內(nèi),也就是說(shuō) vm 模塊沒(méi)有辦法對(duì)異步代碼直接限制執(zhí)行時(shí)間。我們也不能額外通過(guò)一個(gè) timer 去檢查超時(shí),因?yàn)闄z查了執(zhí)行中的 vm 也沒(méi)有方法去中止掉。
另外,在 Node.js 通過(guò) vm.runInContext 看起來(lái)似乎隔離了代碼執(zhí)行環(huán)境,但實(shí)際上卻很容易「逃逸」出去。
const vm = require("vm"); const sandbox = {}; const script = new vm.Script("this.constructor.constructor("return process")().exit()"); const context = vm.createContext(sandbox); script.runInContext(context);
執(zhí)行上邊的代碼,宿主程序立即就會(huì)「退出」,sandbox 是在 VM 之外的環(huán)境創(chuàng)建的,需 VM 中的代碼的 this 指向的也是 sandbox,那么
//this.constructor 就是外所的 Object 構(gòu)建函數(shù) const ObjConstructor = this.constructor; //ObjConstructor 的 constructor 就是外包的 Function const Function = ObjConstructor.constructor; //創(chuàng)建一個(gè)函數(shù),并執(zhí)行它,返回全局 process 全局對(duì)象 const process = (new Function("return process"))(); //退出當(dāng)前進(jìn)程 process.exit();
沒(méi)有人愿意用戶一段腳本就能讓應(yīng)用掛掉吧。除了退出進(jìn)程序之外,實(shí)際上還能干更多的事情。
有個(gè)簡(jiǎn)單的方法就能避免通過(guò) this.constructor 拿到 process,如下:
const vm = require("vm"); //創(chuàng)建一外無(wú) proto 的空白對(duì)象作為 sandbox const sandbox = Object.create(null); const script = new vm.Script("..."); const context = vm.createContext(sandbox); script.runInContext(context);
但還是有風(fēng)險(xiǎn)的,由于 JavaScript 本身的動(dòng)態(tài)的特點(diǎn),各種黑魔法防不勝防。事實(shí) Node.js 的官方文檔中也提到「 不要把 VM 當(dāng)做一個(gè)安全的沙箱,去執(zhí)行任意非信任的代碼」。
有哪些做了進(jìn)一步工作的社區(qū)模塊?
在社區(qū)中有一些開源的模塊用于運(yùn)行不信任代碼,例如 sandbox、vm2、jailed 等。相比較而言 vm2 對(duì)各方面做了更多的安全工作,相對(duì)安全些。
從 vm2 的官方 README 中可以看到,它基于 Node.js 內(nèi)建的 VM 模塊,來(lái)建立基礎(chǔ)的沙箱環(huán)境,然后同時(shí)使用上了文介紹過(guò)的 ES6 的 Proxy 技術(shù)來(lái)防止沙箱腳本逃逸。
用同樣的測(cè)試代碼來(lái)試試 vm2
const { VM } = require("vm2"); new VM().run("this.constructor.constructor("return process")().exit()");
如上代碼,并沒(méi)有成功結(jié)束掉宿主程序,vm2 官方 REAME 中說(shuō)「vm2 是一個(gè)沙盒,可以在 Node.js 中按全的執(zhí)行不受信任的代碼」。
然而,事實(shí)上我們還是可以干一些「壞」事情,比如:
const { VM } = require("vm2"); const vm = new VM({ timeout: 1000, sandbox: {}}); vm.run("new Promise(()=>{})");
上邊的代碼將永遠(yuǎn)不會(huì)執(zhí)行結(jié)束,如同 Node.js 內(nèi)建模塊一樣 vm2 的 timeout 對(duì)異步操作是無(wú)效的。同時(shí),vm2 也不能額外通過(guò)一個(gè) timer 去檢查超時(shí),因?yàn)樗矝](méi)有辦法將執(zhí)行中的 vm 終止掉。這會(huì)一點(diǎn)點(diǎn)耗費(fèi)完服務(wù)器的資源,讓你的應(yīng)用掛掉。
那么或許你會(huì)想,我們能不能在上邊的 sandbox 中放一個(gè)假的 Promise 從而禁掉 Promise 呢?答案是能提供一個(gè)「假」的 Promise,但卻沒(méi)有辦法完成禁掉 Promise,比如
const { VM } = require("vm2"); const vm = new VM({ timeout: 1000, sandbox: { Promise: function(){}} }); vm.run("Promise = (async function(){})().constructor;new Promise(()=>{});");
可以看到通過(guò)一行 Promise = (async function(){})().constructor 就可以輕松再次拿到 Promise 了。從另一個(gè)層面來(lái)看,況且或許有時(shí)我們還想讓自定義腳本支持異步處理呢。
如何建立一個(gè)更安全一些的沙箱?通過(guò)上文的探究,我們并沒(méi)有找到一個(gè)完美的方案在 Node.js 建立安全的隔離的沙箱。其中 vm2 做了不少處理,相對(duì)來(lái)講算是較安全的方案了,但問(wèn)題也很明顯,比如異步不能檢查超時(shí)的問(wèn)題、和宿主程序在相同進(jìn)程的問(wèn)題。
沒(méi)有進(jìn)程隔離時(shí),通過(guò) VM 創(chuàng)建的 sanbox 大體是這樣的
那么,我們是不是可以嘗試,將非受信代碼,通過(guò) vm2 這個(gè)模塊隔離在一個(gè)獨(dú)立的進(jìn)程中執(zhí)行呢?然后,執(zhí)行超時(shí)時(shí),直接將隔離的進(jìn)程干掉,但這里我們需要考慮如下幾個(gè)問(wèn)題
通過(guò)進(jìn)程池統(tǒng)一調(diào)度管理沙箱進(jìn)程
如果來(lái)一個(gè)執(zhí)行任務(wù),創(chuàng)建一個(gè)進(jìn)程,用完銷毀,僅處理進(jìn)程的開銷就已經(jīng)稍大了,并且也不能不設(shè)限的開新進(jìn)程和宿主應(yīng)用搶資源,那么,需要建一個(gè)進(jìn)程池,所有任務(wù)到來(lái)會(huì)創(chuàng)建一個(gè) Script 實(shí)例,先進(jìn)入一個(gè) pending 隊(duì)列,然后直接將 script 實(shí)例的 defer 對(duì)象返回,調(diào)用處就能 await 執(zhí)行結(jié)果了,然后由 sandbox master 根據(jù)工程進(jìn)程的空閑程序來(lái)調(diào)度執(zhí)行,master 會(huì)將 script 的執(zhí)行信息,包括重要的 ScriptId,發(fā)送給空閑的 worker,worker 執(zhí)行完成后會(huì)將「結(jié)果 + script 信息」回傳給 master,master 通過(guò) ScriptId 識(shí)別是哪個(gè)腳本執(zhí)行完畢了,就是結(jié)果進(jìn)行 resolve 或 reject 處理。
這樣,通過(guò)「進(jìn)程池」即能降低「進(jìn)程來(lái)回創(chuàng)建和銷毀的開銷」,也能確保不過(guò)度搶占宿主資源,同時(shí),在異步操作超時(shí),還能將工程進(jìn)程直接殺掉,同時(shí),master 將發(fā)現(xiàn)一個(gè)工程進(jìn)程掛掉,會(huì)立即創(chuàng)建替補(bǔ)進(jìn)程。
處理的數(shù)據(jù)和結(jié)果,還有公開給沙箱的方法
進(jìn)程間如何通訊,需要「動(dòng)態(tài)代碼」處理數(shù)據(jù)可以直接序列化后通過(guò) IPC 發(fā)送給隔離 Sandbox 進(jìn)程,執(zhí)行結(jié)果一樣經(jīng)過(guò)序列化通過(guò) IPC 傳輸。
其中,如果想法公開一個(gè)方法給 sandbox,因?yàn)椴辉谝粋€(gè)進(jìn)程,并不能方便的將一個(gè)方案的引用傳遞給 sandbox。我們可以將宿主的方法,在傳遞給 sandbox worker 之類做一下處理,轉(zhuǎn)換為一個(gè)「描述對(duì)象」,包括了允許 sandbox 調(diào)用的方法信息,然后將信息,如同其它數(shù)據(jù)一樣發(fā)送給 worker 進(jìn)程,worker 收到數(shù)據(jù)后,識(shí)出來(lái)所「方法描述對(duì)象」,然后在 worker 進(jìn)程中的 sandbox 對(duì)象上建立代理方法,代理方法同樣通過(guò) IPC 和 master 通訊。
針對(duì)沙箱進(jìn)程進(jìn)行 CPU 和內(nèi)存配額限制
在 Linux 平臺(tái),通過(guò) CGoups 對(duì)沙箱進(jìn)程進(jìn)行整體的 CPU 和內(nèi)存等資源的配額限制,Cgroups 是 Control Groups 的縮寫,是 Linux 內(nèi)核提供的一種可以限制、記錄、隔離進(jìn)程組(Process Groups)所使用的物理資源(如:CPU、Memory,IO 等等)的機(jī)制。最初由 Google 的工程師提出,后來(lái)被整合進(jìn) Linux 內(nèi)核。Cgroups 也是 LXC 為實(shí)現(xiàn)虛擬化所使用的資源管理手段,可以說(shuō)沒(méi)有 CGroups 就沒(méi)有 LXC。
最終,我們建立了一個(gè)大約這樣的「沙箱環(huán)境」
如此這般處理起來(lái)是不是感覺(jué)很麻煩?但我們就有了一個(gè)更加安全一些的沙箱環(huán)境了,這些處理。筆者已經(jīng)基于 TypeScript 編寫,并封裝為一個(gè)獨(dú)立的模塊 Safeify。
相較于內(nèi)建的 VM 及常見的幾個(gè)沙箱模塊, Safeify 具有如下特點(diǎn):
為將要執(zhí)行的動(dòng)態(tài)代碼建立專門的進(jìn)程池,與宿主應(yīng)用程序分離在不同的進(jìn)程中執(zhí)行
支持配置沙箱進(jìn)程池的最大進(jìn)程數(shù)量
支持限定同步代碼的最大執(zhí)行時(shí)間,同時(shí)也支持限定包括異步代碼在內(nèi)的執(zhí)行時(shí)間
支持限定沙箱進(jìn)程池的整體的 CPU 資源配額(小數(shù))
支持限定沙箱進(jìn)程池的整體的最大的內(nèi)存限制(單位 m)
GitHub: https://github.com/Houfeng/sa... ,歡迎 Star & Issues
最后,簡(jiǎn)單介紹一下 Safeify 如何使用,通過(guò)如下命令安裝
npm i safeify --save
在應(yīng)用中使用,還是比較簡(jiǎn)單的,如下代碼(TypeScript 中類似)
import { Safeify } from "./Safeify"; const safeVm = new Safeify({ timeout: 50, //超時(shí)時(shí)間,默認(rèn) 50ms asyncTimeout: 500, //包含異步操作的超時(shí)時(shí)間,默認(rèn) 500ms quantity: 4, //沙箱進(jìn)程數(shù)量,默認(rèn)同 CPU 核數(shù) memoryQuota: 500, //沙箱最大能使用的內(nèi)存(單位 m),默認(rèn) 500m cpuQuota: 0.5, //沙箱的 cpu 資源配額(百分比),默認(rèn) 50% }); const context = { a: 1, b: 2, add(a, b) { return a + b; } }; const rs = await safeVm.run(`return add(a,b)`, context); console.log("result",rs);
關(guān)于安全的問(wèn)題,沒(méi)有最安全,只有更安全,Safeify 已在一個(gè)項(xiàng)目中使用,但自定義腳本的功能是僅針對(duì)內(nèi)網(wǎng)用戶,有不少動(dòng)態(tài)執(zhí)行代碼的場(chǎng)景其實(shí)是可以避免的,繞不開或?qū)嵲谛枰峁┻@個(gè)功能時(shí),希望本文或 Safeify 能對(duì)大家有所幫助就行了。
-- end --
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/94527.html
摘要:而標(biāo)準(zhǔn)庫(kù)中的是不安全的,用戶腳本可以輕易突破沙箱環(huán)境,獲取主程序的上述代碼在執(zhí)行時(shí),程序在第二行就直接退出,虛擬機(jī)環(huán)境中的代碼逃逸,獲得了主線程的變量,并調(diào)用,造成主程序非正常退出。 NPM酷庫(kù),每天兩分鐘,了解一個(gè)流行NPM庫(kù)。 今天我們要了解的庫(kù)是 vm2,則是一個(gè)Node.js 官方 vm 庫(kù)的替代品,主要解決了安全問(wèn)題。 不安全的vm 在Node.js官方標(biāo)準(zhǔn)庫(kù)中有一個(gè)vm庫(kù),...
摘要:中華人民共和國(guó)數(shù)據(jù)安全法關(guān)鍵信息基礎(chǔ)設(shè)施安全保護(hù)條例都將于年月日起開始施行,中華人民共和國(guó)個(gè)人信息保護(hù)法也在最近正式發(fā)布,將于年月日起施行。毫無(wú)疑問(wèn),隱私計(jì)算是一塊市場(chǎng)大蛋糕。 《中華人民共和國(guó)數(shù)據(jù)安全法》、《關(guān)鍵信息基礎(chǔ)設(shè)施安全保護(hù)條例》都將于2021年9月1日起開始施行,《中華人民共和國(guó)個(gè)人信息保護(hù)法》也在最近正式發(fā)布,將于2021年11月1日起施行。我國(guó)在安全立法方面三箭齊發(fā),顯示出...
摘要:第一個(gè)主要的包管理器在被引用后不久就搭建起來(lái)了,并且迅速成為世界上最受歡迎的包管理器之一。簡(jiǎn)介是一款新的包管理器,在取代客戶端和其他包管理器現(xiàn)有工作流的同時(shí),又保留了對(duì)代理的兼容性。 在JavaScript社區(qū),工程師們分享了成百上千的代碼段,我們不用自己從頭編寫基礎(chǔ)組件、類庫(kù)或者框架。反過(guò)來(lái),每段代碼又或許依賴于其它的代碼段,而這些依賴就是通過(guò) package managers(包管...
摘要:書接上文瀏覽器之引擎本章主要講解瀏覽器安全機(jī)制的網(wǎng)頁(yè)的安全和瀏覽器的安全??偨Y(jié)瀏覽器的安全機(jī)制包括網(wǎng)頁(yè)安全模型和沙箱模型其中網(wǎng)頁(yè)安全模型就是利用了同源策略,讓不同域中的網(wǎng)頁(yè)不能相互訪問(wèn),當(dāng)然有好幾種瀏覽器跨域的方法可以其相互訪問(wèn)。 showImg(https://segmentfault.com/img/remote/1460000016375575); 前言 此文章是我最近在看的【W(wǎng)...
閱讀 593·2021-11-22 14:45
閱讀 3086·2021-10-15 09:41
閱讀 1585·2021-10-11 10:58
閱讀 2807·2021-09-04 16:45
閱讀 2621·2021-09-03 10:45
閱讀 3252·2019-08-30 15:53
閱讀 1233·2019-08-29 12:28
閱讀 2146·2019-08-29 12:14