From c20cc595e2c23eac6e12024c239e477b79d50a63 Mon Sep 17 00:00:00 2001 From: Innei Date: Mon, 14 Mar 2022 14:50:39 +0800 Subject: [PATCH] fix: serverless vm2 safe-eval --- package.json | 1 + pnpm-lock.yaml | 16 +++++++ src/modules/serverless/serverless.readme.md | 4 +- src/modules/serverless/serverless.service.ts | 12 +++++- src/utils/safe-eval.util.ts | 44 +++++++------------- test/src/utils/safe-eval.spec.ts | 12 ++++-- 6 files changed, 56 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 5d5f84d5..63b0a6bc 100644 --- a/package.json +++ b/package.json @@ -124,6 +124,7 @@ "rxjs": "7.5.5", "snakecase-keys": "5.1.2", "ua-parser-js": "1.0.2", + "vm2": "3.9.9", "xss": "1.0.11", "zx": "4.3.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e2db030c..c40f70e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,6 +111,7 @@ specifiers: tsconfig-paths: 3.13.0 typescript: 4.6.2 ua-parser-js: 1.0.2 + vm2: 3.9.9 webpack-node-externals: 3.0.0 xss: 1.0.11 zx: 4.3.0 @@ -177,6 +178,7 @@ dependencies: rxjs: 7.5.5 snakecase-keys: 5.1.2 ua-parser-js: 1.0.2 + vm2: 3.9.9 xss: 1.0.11 zx: 4.3.0 @@ -2317,6 +2319,11 @@ packages: engines: {node: '>=0.4.0'} dev: true + /acorn-walk/8.2.0: + resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + engines: {node: '>=0.4.0'} + dev: false + /acorn/7.4.1: resolution: {integrity: sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==} engines: {node: '>=0.4.0'} @@ -8167,6 +8174,15 @@ packages: extsprintf: 1.3.0 dev: false + /vm2/3.9.9: + resolution: {integrity: sha512-xwTm7NLh/uOjARRBs8/95H0e8fT3Ukw5D/JJWhxMbhKzNh1Nu981jQKvkep9iKYNxzlVrdzD0mlBGkDKZWprlw==} + engines: {node: '>=6.0'} + hasBin: true + dependencies: + acorn: 8.7.0 + acorn-walk: 8.2.0 + dev: false + /w3c-hr-time/1.0.2: resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==} dependencies: diff --git a/src/modules/serverless/serverless.readme.md b/src/modules/serverless/serverless.readme.md index 97de43b7..648ecece 100644 --- a/src/modules/serverless/serverless.readme.md +++ b/src/modules/serverless/serverless.readme.md @@ -56,12 +56,14 @@ const remoteModule = 受信任的三方库,可在 `snippet.service.ts` 中找到。 -**注意**:这是一个完全的执行上下文,你不能编写某些在 NodeJS 运行时正常执行的代码。 +**注意**:这是一个完全隔离(可能存在逃逸,请及时指出)的执行上下文,你不能编写某些在 NodeJS 运行时正常执行的代码。 比如: `process` 中只有只读的 env 可以获取,其他方法都被移除; `setTimeout` 等 API 被移除。但是你可以在独立模块中使用这些 API,需要注意,内存泄漏和安全性。 `require(id, useCache)` require 支持第二个参数,默认为 true,这是 NodeJS 的默认行为,可以设定为 `false` 以禁用 `require` 的缓存,但是会增加性能开销。 +**注意**:你仍然可以在独立模块中使用主线程的 `require` 方法,所以这并不是一个真正隔离的环境。在使用第三方模块和请注意安全。请不要使用不受信任的模块。 + ## `Context` `handler` 函数的第一个参数接受一个全局上下文对象。 diff --git a/src/modules/serverless/serverless.service.ts b/src/modules/serverless/serverless.service.ts index c4960d1c..35fa9709 100644 --- a/src/modules/serverless/serverless.service.ts +++ b/src/modules/serverless/serverless.service.ts @@ -273,7 +273,17 @@ export class ServerlessService { // } // fin. is built-in module - const module = isBuiltinModule(id, ['fs', 'os', 'child_process', 'sys']) + const module = isBuiltinModule(id, [ + 'fs', + 'os', + 'child_process', + 'sys', + 'process', + 'vm', + 'v8', + 'cluster', + 'fs/promises', + ]) if (!module) { throw new Error(`cannot require ${id}`) } else { diff --git a/src/utils/safe-eval.util.ts b/src/utils/safe-eval.util.ts index 8e429900..e1fbf7f4 100644 --- a/src/utils/safe-eval.util.ts +++ b/src/utils/safe-eval.util.ts @@ -1,34 +1,22 @@ -import { customAlphabet } from 'nanoid' -import vm from 'vm' -const nanoid = customAlphabet( - '0123456789_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ$', - 16, -) -export function safeEval( - code: string, - context = {}, - opts?: string | vm.RunningScriptOptions, -) { - const sandbox = {} - const resultKey = 'SAFE_EVAL_' + nanoid() - sandbox[resultKey] = {} - const clearContext = ` - (function() { - Function = undefined; - const keys = Object.getOwnPropertyNames(this).concat(['constructor']); - keys.forEach((key) => { - const item = this[key]; - if (!item || typeof item.constructor !== 'function') return; - this[key].constructor = undefined; - }); - })(); - ` - code = clearContext + resultKey + '=' + `((() => { ${code} })())` +import vm2 from 'vm2' +export function safeEval(code: string, context = {}) { + const sandbox = { + global: {}, + } + + code = `((() => { ${code} })())` if (context) { Object.keys(context).forEach(function (key) { sandbox[key] = context[key] }) } - vm.runInNewContext(code, sandbox, opts) - return sandbox[resultKey] + + const VM = new vm2.VM({ + timeout: 60_0000, + sandbox, + + eval: false, + }) + + return VM.run(code) } diff --git a/test/src/utils/safe-eval.spec.ts b/test/src/utils/safe-eval.spec.ts index f8ef5e11..ce92537a 100644 --- a/test/src/utils/safe-eval.spec.ts +++ b/test/src/utils/safe-eval.spec.ts @@ -12,9 +12,7 @@ describe.only('test safe-eval', () => { }) it('should can not access to global or process or require', () => { - expect(() => { - safeEval(`return global`) - }).toThrow() + expect(safeEval(`return global`)).toStrictEqual({}) expect(() => { safeEval(`return process`) @@ -25,6 +23,14 @@ describe.only('test safe-eval', () => { }).toThrow() }) + describe('test escape', () => { + it('case1', () => { + expect(() => + safeEval(`this.constructor.constructor("return process")().exit()`), + ).toThrow() + }) + }) + it('should can access mocked global context', () => { const res = safeEval(`return global.a`, { global: { a: 1 } }) expect(res).toBe(1)