動態執行腳本

提到動態執行腳本,大家想到的肯定是 evalnew Function(),在 nodejs 中有專屬的 vm 模塊,可以完成相應的 sandbox 作用。

瀏覽器中動態執行腳本

eval()

函數會將傳入的字符串當做 JavaScript 代碼進行執行,返回字符串中代碼的返回值;如果參數不是字符串將原封不動返回。

如果你間接的使用 eval(),比如通過一個引用來調用它,而不是直接的調用 eval。從 ECMAScript 5 起,它工作在全局作用域下,而不是局部作用域中。

function test() {
  let x = 2, y = 4;
  console.log(eval('x + y'));  // 直接調用,使用本地作用域,結果是 6
  let geval = eval; // 等價於在全局作用域調用
  console.log(geval('x + y')); // 間接調用,使用全局作用域,throws ReferenceError 因爲`x`未定義
  (0, eval)('x + y'); // 另一個間接調用的例子
}

eval 中函數作爲字符串被定義需要“(”和“)”作爲前綴和後綴

let fctStr1 = 'function a() {}'
let fctStr2 = '(function a() {})'
let fct1 = eval(fctStr1)  // 返回undefined
let fct2 = eval(fctStr2)  // 返回一個函數

MDN 建議永遠不要使用 eval

  • eval() 使用與調用者相同的權限執行代碼。如果你用 eval() 運行的字符串代碼被惡意方(不懷好意的人)修改,您最終可能會在您的網頁/擴展程序的權限下,在用戶計算機上運行惡意代碼。更重要的是,第三方代碼可以看到某一個 eval() 被調用時的作用域,這也有可能導致一些不同方式的攻擊。

  • eval() 通常比其他替代方法更慢,因爲它必須調用 JS 解釋器,而許多其他結構則可被現代 JS 引擎進行優化。此外,現代JavaScript解釋器將javascript轉換爲機器代碼。 這意味着任何變量命名的概念都會被刪除。 因此,任意一個eval的使用都會強制瀏覽器進行冗長的變量名稱查找,以確定變量在機器代碼中的位置並設置其值。

Function 是替代 eval 的一個好的方法。

Function

new Function ([arg1[, arg2[, ...argN]],] functionBody)

每個 JavaScript 函數實際上都是一個 Function 對象。運行 (function(){}).constructor === Function // true 便可以得到這個結論。

eval 不同的是,Function 創建的函數只能在全局作用域中運行。

function test() {
  let x = 2, y = 4;
  console.log(new Function('return x + y')());  // 直接調用,使用全局作用域,throws ReferenceError
}

Nodejs 動態執行腳本

通過 node 的核心模塊 vm 來實現。vm可以使用v8的Virtual Machine contexts動態地編譯和執行代碼,而代碼的執行上下文是與當前進程隔離的,但是這裏的隔離並不是絕對的安全,不完全等同瀏覽器的沙箱環境。

  1. vm.runInContext(code, contextifiedObject[, options])

    在指定的 contextifiedObject 的上下文裏執行它並返回其結果。 被執行的代碼無法獲取本地作用域。 contextifiedObject 必須是事先被 vm.createContext() 方法上下文隔離化過的對象。

    const vm = require('vm')
    
    const contextObject = { a: 1 }
    vm.createContext(contextObject)
    const result = vm.runInContext('a += 1; b = 3', contextObject)
    console.log(result) // 3 { a: 2, b: 3 }
    
  2. vm.runInNewContext(code[, contextObject[, options]])

    給指定的 contextObject(若爲 undefined,則會新建一個contextObject)提供一個隔離的上下文, 再在此上下文中執行編譯的 code,最後返回結果。 運行中的代碼無法獲取本地作用域。

    const vm = require('vm')
    
    const result = vm.runInNewContext('a += 1; b = 3', {a: 1})
    console.log(result)	// 3 { a: 2, b: 3 }
    
  3. vm.runInThisContext(code[, options])

    在當前的 global 對象的上下文中編譯並執行 code,最後返回結果。 運行中的代碼無法獲取本地作用域,但可以獲取當前的 global 對象。

    global.a = 1
    const result = vm.runInThisContext('a += 1')
    console.log(result)
    

    vm.runInThisContext() 更像是間接的執行 eval(), 就像 (0,eval)('code')

  4. eval()

    Nodejs 中同樣可以使用 eval 函數,但性能和安全性有差異。請查看 https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/

  5. vm2

    Node.js 的高級 vm/sandbox,https://github.com/patriksimek/vm2

上下文隔離化

所有用 Node.js 所運行的 JavaScript 代碼都是在一個“上下文”的作用域中被執行的。在 V8 中,一個上下文是一個執行環境,它允許分離的,無關的 JavaScript 應用在一個 V8 的單例中被運行。 必須明確地指定用於運行所有 JavaScript 代碼的上下文。

vm.createContext([contextObject[, options]])

contextObject參數(如果 contextObjectundefined,則爲新創建的對象)在內部與 V8 上下文的新實例相關聯。 該 V8 上下文提供了使用 vm 模塊的方法運行的 code 以及可在其中運行的隔離的全局環境。

使用場景

動態執行字符串代碼。vue ssr 中是通過 runInNewContext 實現的( Vue SSR 指南)。

參考地址

  • https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
  • http://nodejs.cn/api/vm.html
  • https://ssr.vuejs.org/zh/api/#runinnewcontext
  • https://odino.org/eval-no-more-understanding-vm-vm2-nodejs/
  • https://github.com/patriksimek/vm2
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章