node.js 沙盒逃逸分析

做者: 凹凸曼 - nobonode

背景

平常開發需求中有時候爲了追求靈活性或下降開發難度,會在業務代碼裏直接使用 eval/Function/vm 等功能,其中 eval/Function 算是動態執行 JS,但沒法屏蔽當前執行環境的上下文,但 node.js 裏提供了 vm 模塊,至關於一個虛擬機,可讓你在執行代碼時候隔離當前的執行環境,避免被惡意代碼攻擊。安全

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 遊戲、追求高度靈活配置的場景。

解決方案

  • 事前處理,如:代碼安全掃描、語法限制
  • 使用 vm2 模塊,它的本質就是經過代理的方式來進行安全校驗,雖然也可能還存在未出現的逃逸方式,因此在使用時也謹慎對待。
  • 本身實現解釋器,並在解釋器層接管全部對象建立及屬性訪問。

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章。

相關文章
相關標籤/搜索