做者: 凹凸曼 - nobonode
平常開發需求中有時候爲了追求靈活性或下降開發難度,會在業務代碼裏直接使用 eval/Function/vm 等功能,其中 eval/Function 算是動態執行 JS,但沒法屏蔽當前執行環境的上下文,但 node.js 裏提供了 vm 模塊,至關於一個虛擬機,可讓你在執行代碼時候隔離當前的執行環境,避免被惡意代碼攻擊。安全
vm 模塊可在 V8 虛擬機上下文中編譯和運行代碼,虛擬機上下文可自行配置,利用該特性作到沙盒的效果。例如:ui
const vm = require("vm"); const x = 1; const y = 2; const context = { x: 2, console }; vm.createContext(context); // 上下文隔離化對象。 const code = "console.log(x); console.log(y)"; vm.runInContext(code, context); // 輸出 2 // Uncaught ReferenceError: y is not defin
根據以上示例,能夠看出和 eval/Function 最大的區別就是可自定義上下文,也就能夠控制被執行代碼的訪問資源。例如以上示例,除了語言的語法、內置對象等,沒法訪問到超出上下文外的任何信息,因此示例中出現了錯誤提示: y 未定義。如下是 vm 的的執行示例圖:this
沙盒環境代碼只能讀取 VM 上下文 數據。spa
node.js 在 vm 的文檔頁上有以下描述:prototype
vm 模塊不是安全的機制。 不要使用它來運行不受信任的代碼。代理
剛開始看到這句話的很好奇,爲何會這樣?按照剛纔的理解他應該是安全的?搜索後咱們找到一段逃逸示例:code
const vm = require("vm"); const ctx = {}; vm.runInNewContext( 'this.constructor.constructor("return process")().exit()', ctx ); console.log("Never gets executed.");
以上示例中 this 指向 ctx 並經過原型鏈的方式拿到沙盒外的 Funtion,完成逃逸,並執行逃逸後的 JS 代碼。對象
以上示例大體拆分:blog
tmp = ctx.constructor; // Object exec = tmp.constructor; // Function exec("return Process");
以上是經過原型鏈方式完成逃逸,若是將上下文對象的原型鏈設置爲 null 呢?
const ctx = Object.create(null);
這時沙盒在經過 ctx.constructor,就會出錯,也就沒法完成沙盒逃逸,完整示例以下:
const vm = require("vm"); const ctx = Object.create(null); vm.runInNewContext( 'this.constructor.constructor("return process")().exit()', ctx ); // throw Error
但,真的這樣簡單嗎?
再來看看如下成功逃逸示例:
const vm = require("vm"); const ctx = Object.create(null); ctx.data = {}; vm.runInNewContext( 'this.data.constructor.constructor("return process")().exit()', ctx ); // 逃逸成功! console.log("Never gets executed.");
爲何會這樣?
緣由
因爲 JS 裏全部對象的原型鏈都會指向 Object.prototype,且 Object.prototype 和 Function 之間是相互指向的,全部對象經過原型鏈都能拿到 Function,最終完成沙盒逃逸並執行代碼。
逃逸後代碼能夠執行以下代碼拿到 require,從而並加載其餘模塊功能,示例:
const vm = require("vm"); const ctx = { console, }; vm.runInNewContext( ` var exec = this.constructor.constructor; var require = exec('return process.mainModule.constructor._load')(); console.log(require('fs')); `, ctx );
沙盒執行上下文是隔離的,但可經過原型鏈的方式獲取到沙盒外的 Function,從而完成逃逸,拿到全局數據,示例圖以下:
因爲語言的特性,在沙盒環境下經過原型鏈的方式能獲取全局的 Function,並經過它來執行代碼。
最終確實如官方所說,在使用 vm 的時應確保所運行的代碼是可信任的。
eval/Function/vm 等可動態執行代碼的功能在 JavaScript 裏必定是用來執行可信任代碼。
如下多是比較常見會用到動態執行腳本的場景:模板引擎,H5 遊戲、追求高度靈活配置的場景。
歡迎關注凹凸實驗室博客:aotu.io
或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章。