在一些系統中,咱們但願給用戶提供插入自定義邏輯的能力,除了 RPC
和 REST
以外,運行客戶提供的代碼也是比較經常使用的方法,好處是能夠極大地減小在網絡上的耗時。JavaScript 是一種很是流行並且容易上手的語言,所以,讓用戶用 JavaScript 來寫自定義邏輯是一個不錯的選擇。下面咱們介紹 Node.js 提供的 vm 模塊以及分析用它來運行不信任代碼可能遇到的問題。php
vm 模塊是 Node.js 內置的核心模塊,它能讓咱們編譯 JavaScript 代碼和在指定的環境中運行。請看下面例子:html
const util = require('util'); const vm = require('vm'); // 1. 建立一個 vm.Script 實例, 編譯要執行的代碼 const script = new vm.Script('globalVar += 1; anotherGlobalVar = 1; '); // 2. 用於綁定到 context 的對象 const sandbox = {globalVar: 1}; // 3. 建立一個 context, 而且把 sandbox 這個對象綁定到這個環境, 做爲全局對象 const contextifiedSandbox = vm.createContext(sandbox); // 4. 運行上面編譯的代碼, context 是 contextifiedSandbox const result = script.runInContext(contextifiedSandbox); console.log(`sandbox === contextifiedSandbox ? ${sandbox === contextifiedSandbox}`); // sandbox === contextifiedSandbox ? true console.log(`sandbox: ${util.inspect(sandbox)}`); // sandbox: { globalVar: 2, anotherGlobalVar: 1 } console.log(`result: ${util.inspect(result)}`); // result: 1
vm.Script
是一個類,用於建立代碼實例,後面能夠屢次運行。node
vm.createContext(sandbox)
用於 "contextify" 一個對象,根據 ECMAScript 2015 語言規範,代碼的執行須要一個 execution context。這裏的 "contextify",就是把傳進去的對象與 V8 的一個新的 context 進行關聯。這裏所說的關聯,個人理解是,這個 "contextified" 對象的屬性將會成爲那個 context 的全局屬性,同時,在 context 下運行代碼時產生的全局屬性也會成爲這個 "contextified" 對象的屬性。linux
script.runInContext(contextifiedSandbox)
就是使代碼在 contextifiedSandbox
這個 context 中運行,從上面的輸出能夠看到,代碼運行後,contextifiedSandbox
裏面的屬性的值已經被改變了,運行結果是最後一個表達式的值。 git
除了上面幾個接口以外,vm 模塊還有一些更便捷的接口,例如 vm.runInContext(code, contextifiedSandbox[, options])
,vm.runInNewContext(code[, sandbox][, options])
等,詳細可看文檔。github
咱們用 vm 運行代碼的時候極可能須要獲得一些結果,從上面的例子中能夠看到,咱們能夠經過把結果做爲最後一個表達式的值傳給外層,或者做爲context
的屬性給外層使用,這在同步代碼裏沒有問題,可是假如結果須要依賴裏面的異步操做呢?這時,咱們能夠經過在 context
裏放一個回調函數。 下面是例子:api
const util = require('util'); const vm = require('vm'); const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) { console.log(result); }}; vm.createContext(sandbox); const script = new vm.Script(` setTimeout(function(){ globalVar++; cb("async result"); }, 1000); `,{}); script.runInContext(sandbox); console.log(`globalVar: ${sandbox.globalVar}`); // globalVar: 1 // async result
script.runInContext(contextifiedSandbox[, options])
方法有一個 timeout
選項能夠設定代碼的運行時間,若是超過期間就會拋出錯誤,請看下面例子: 安全
const util = require('util'); const vm = require('vm'); const sandbox = {}; const contextifiedSandbox = vm.createContext(sandbox); const script = new vm.Script('while(true){}'); const result = script.runInContext(contextifiedSandbox, {timeout: 1000}); // const result = script.runInContext(contextifiedSandbox, {timeout: 1000}); // ^ // Error: Script execution timed out.
再試試異步代碼,網絡
const util = require('util'); const vm = require('vm'); const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) { console.log(result); }}; vm.createContext(sandbox); const script = new vm.Script(` setTimeout(function(){ globalVar++; cb("async result"); }, 1000); globalVar; `,{}); const result = script.runInContext(sandbox, {timeout: 500}); console.log(`result: ${result}`); // result: 1 // async result
沒有錯誤拋出,也就是說,這個選項並不能限制異步代碼的運行時間,那應該怎麼去限制全部代碼的執行時間呢,目前好像沒有接口終止 vm 代碼的運行,若是有異步代碼長時間不結束,很容易形成內存泄露,目前可行的方案是使用子進程去運行代碼,若是超過限定時間尚未結果,就殺掉該子進程,另外,使用子進程還能夠更方便地對內存等資源進行限制。異步
在一個全新的 V8 context 裏運行代碼,裏面包含了語言規範規定的內置的一些函數和對象,若是咱們想要一些語言規範以外的功能或者模塊,咱們須要把相應對象放到與這個 context 關聯的對象裏,例如在上面例子中的這句代碼:
const sandbox = {globalVar: 1, setTimeout: setTimeout, cb: function(result) { console.log(result); }};
setTimeout
不是語言規範規定的內置函數, context 自己不提供,因此咱們須要經過關聯的對象傳進去。
然而,當咱們把一些模塊功能提供給 context 的時候,也同時帶入了更多的安全隱患,請看下面來自例子:
const util = require('util'); const vm = require('vm'); const sandbox = {}; vm.createContext(sandbox); const script = new vm.Script(` // sandbox 的 constructor 是外層的 Object 類 // Object 類的 constructor 是外層的 Function 類 const OutFunction = this.constructor.constructor; // 因而, 利用外層的 Function 構造一個函數就能夠獲得外層的全局 this const OutThis = (OutFunction('return this;'))(); // 獲得 require const require = OutThis.process.mainModule.require; // 試試 require('fs'); `,{}); const result = script.runInContext(sandbox); console.log(result === require('fs')); // true
顯然,定製 context 的時候,任何一個傳進去的對象或者函數均可能帶來上面的問題,安全問題真的有不少工做須要作。
Github 上有一些開源的模塊用於運行不信任代碼,例如 sandbox,vm2,jailed等。查看這些項目的 issue 能夠發現,sandbox 和 jailed 均可以用相似上面的方法突破限制,而 vm2 對這方面作了防禦,其它方面也作了更多的安全工做,相對安全些。
生產中能夠考慮在子進程中運行 vm2, 而後增長更低層的安全限制, 例如限制進程的權限和使用 cgroups 進行 IO,內存等資源限制,這裏不詳細討論。
本文經過幾個例子介紹了 Node.js 的 vm 模塊以及使用 vm 模塊運行不信任代碼可能遇到的問題,而且對安全問題給出了一些建議。
vm
Allowing to terminate a vm context/script
V8 Embedder's Guide
ECMAScript 2015 語言規範
sandbox/issues/50
vm2/issues/32
jailed/issues/33
cgroups