Node.js 沙箱環境

node官方文檔裏提到node的vm模塊能夠用來作沙箱環境執行代碼,沙箱環境針對代碼的上下文環境作了隔離。html

A common use case is to run the code in a sandboxed environment. The sandboxed code uses a different V8 Context, meaning that it has a different global object than the rest of the code.

先看一個例子

const vm = require('vm');
let a = 1;
var result = vm.runInNewContext('var b = 2; a = 3; a + b;', {a});
console.log(result);    // 5
console.log(a);         // 1
console.log(typeof b);  // undefined

沙箱環境中執行的代碼對於外部代碼沒有產生任何影響,不管是新聲明的變量b,仍是從新賦值的變量a。
注意最後一行的代碼默認會被加上return關鍵字,所以無需手動添加,一旦添加的話不會靜默忽略,而是執行報錯。node

const vm = require('vm');
let a = 1;
var result = vm.runInNewContext('var b = 2; a = 3; return a + b;', {a});
console.log(result);
console.log(a);
console.log(typeof b);

以下所示docker

evalmachine.<anonymous>:1
var b = 2; a = 3; return a + b;
                  ^^^^^^

SyntaxError: Illegal return statement
    at new Script (vm.js:74:7)
    at createScript (vm.js:246:10)
    at Object.runInNewContext (vm.js:291:10)
    at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:17)
    at Module._compile (internal/modules/cjs/loader.js:678:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
    at Module.load (internal/modules/cjs/loader.js:589:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
    at Function.Module._load (internal/modules/cjs/loader.js:520:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)

除了runInNewContext外,vm還提供了runInThisContext和runInContext兩個方法均可以用來執行代碼
runInThisContext沒法指定contextbootstrap

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInThisContext('localVar += "vm";');
console.log('vmResult:', vmResult);
console.log('localVar:', localVar);
console.log(global.localVar);

因爲沒法訪問本地的做用域,只能訪問到當前的global對象,所以上面的代碼會由於找不到localVal而報錯segmentfault

evalmachine.<anonymous>:1
localVar += "vm";
^

ReferenceError: localVar is not defined
    at evalmachine.<anonymous>:1:1
    at Script.runInThisContext (vm.js:91:20)
    at Object.runInThisContext (vm.js:298:38)
    at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:21)
    at Module._compile (internal/modules/cjs/loader.js:678:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
    at Module.load (internal/modules/cjs/loader.js:589:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
    at Function.Module._load (internal/modules/cjs/loader.js:520:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)

若是咱們把要執行的代碼改爲直接賦值的話就能夠正常運行了,可是也產生了全局污染(全局的localVar變量)api

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInThisContext('localVar = "vm";');
console.log('vmResult:', vmResult);   // vm
console.log('localVar:', localVar);   // initial value
console.log(global.localVar);         // vm

runInContext在傳入context參數上與runInNewContext有所區別
runInContext傳入的context對象不爲空並且必須是經vm.createContext()處理過的,不然會報錯。
runInNewContext的context參數是非必須的,並且無需通過vm.createContext處理。
runInNewContext和runInContext由於有指定context,因此不會向runInThisContext那樣產生全局污染(不會產生全局的localVar變量)安全

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInNewContext('localVar = "vm";');
console.log('vmResult:', vmResult);   // vm
console.log('localVar:', localVar);   // initial value
console.log(global.localVar);         // undefined

當須要一個沙箱環境執行多個腳本片斷的時候,能夠經過屢次調用runInContext方法可是傳入同一個vm.createContext()返回值實現。app

超時控制及錯誤捕獲

vm針對要執行的代碼提供了超時機制,經過指定timeout參數便可以runInThisContext爲例異步

const vm = require('vm');
let localVar = 'initial value';​
const vmResult = vm.runInThisContext('while(true) { 1 }; localVar = "vm";', {  timeout: 1000});
vm.js:91
      return super.runInThisContext(...args);
                   ^

Error: Script execution timed out.
    at Script.runInThisContext (vm.js:91:20)
    at Object.runInThisContext (vm.js:298:38)
    at Object.<anonymous> (/Users/xiji/workspace/learn/script.js:3:21)
    at Module._compile (internal/modules/cjs/loader.js:678:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:689:10)
    at Module.load (internal/modules/cjs/loader.js:589:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:528:12)
    at Function.Module._load (internal/modules/cjs/loader.js:520:3)
    at Function.Module.runMain (internal/modules/cjs/loader.js:719:10)
    at startup (internal/bootstrap/node.js:228:19)

能夠經過try catch來捕獲代碼錯誤async

const vm = require('vm');
let localVar = 'initial value';​
try {  
    const vmResult = vm.runInThisContext('while(true) { 1 }; localVar = "vm";', {
        timeout: 1000
    });
} catch(e) {  
    console.error('executed code timeout');
}

延遲執行

vm除了即時執行代碼以外,也能夠先編譯而後過一段時間再執行,這就須要提到vm.Script了。其實不管是runInNewContext、runInThisContext仍是runInThisContext,背後其實都建立了Script,從以前的報錯信息就能夠看出來
接下來咱們就用vm.Script來重寫本文開頭的例子

const vm = require('vm');
let a = 1;
var script = new vm.Script('var b = 2; a = 3; a + b;');
setTimeout(() => {  
    let result  = script.runInNewContext({a});  
    console.log(result);     // 5  
    console.log(a);          // 1  
    console.log(typeof b);   // undefined
}, 300);

除了vm.Script,node在9.6版本中新增了vm.Module也能夠作到延遲執行,vm.Module主要用來支持ES6 module,並且它的context在建立的時候就已經綁定好了,關於vm.Module目前還須要在命令行使用flag來啓用支持

node --experimental-vm-module index.js

vm做爲沙箱環境安全嗎?

vm相對於eval來講更安全一些,由於它隔離了當前的上下文環境了,可是儘管如此依然能夠訪問標準的JS API和全局的NodeJS環境,所以vm並不安全,這個在官方文檔裏就提到了

The vm module is not a security mechanism. Do not use it to run untrusted code

請看下面的例子

const vm = require('vm');
vm.runInNewContext("this.constructor.constructor('return process')().exit()")
console.log("The app goes on...") // 永遠不會輸出

爲了不上面這種狀況,能夠將上下文簡化成只包含基本類型,以下所示

let ctx = Object.create(null);
ctx.a = 1; // ctx上不能包含引用類型的屬性
vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);

針對原生vm存在的這個問題,有人開發了vm2包,能夠避免上述問題,可是也不能說vm2就必定是安全的

const {VM} = require('vm2');
new VM().run('this.constructor.constructor("return process")().exit()');

雖然執行上述代碼沒有問題,可是因爲vm2的timeout對於異步代碼不起做用,因此下面的代碼永遠不會執行結束。

const { VM } = require('vm2');
const vm = new VM({ timeout: 1000, sandbox: {}});
vm.run('new Promise(()=>{})');

即便但願經過從新定義Promise的方式來禁用Promise的話,仍是一個能夠繞過的

const { VM } = require('vm2');
const vm = new VM({ 
  timeout: 1000, sandbox: { Promise: function(){}}
});
vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');

總結

vm提供了一種隔離的方式來執行不可信代碼,可是並非很是完全,針對不可信代碼最好的執行方式仍是「物理隔離」,好比docker容器。

參考資料

https://nodejs.org/dist/lates...
https://60devs.com/executing-...
https://odino.org/eval-no-mor...
https://segmentfault.com/a/11...

相關文章
相關標籤/搜索