Vue+Node::VM 沙盒控制台工具

团队内部开发了很多 Web 平台的工具,由于服务端逻辑比较复杂,生产环境上运行的服务涉及到的一些问题,排查起来相对困难,需要进入到当前服务内部查看运行情况

本文提供一种后门服务:在 NodeJS 服务中包装 Node::VM 沙盒模式,支持远程运行在前端编辑器上编写的代码

源代码参考

沙盒流程

figure_0

首先前端用 vue 支持了一个代码编辑器,通过 axios api 请求将代码发送到 node server 端,在 vm 执行,然后返回执行结果给前端

Vue 前端代码

前端代码比较简单,是基于 vue 技术栈对 CodeEditor 做二次封装 source

Node 后端代码

后端主要对 node:vm 的封装 source

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
const globalConsole = global.console

// 沙盒运行结果封装
const sandboxConsole = (level: string, msg: any, ...args: any[]) => {
console.result += `${new Date().toLocaleString()} ${level}:\n${msg}`
args.forEach(item => {
if (typeof item !== 'object') {
console.result += ` ${item}`
} else {
console.result += ` ${JSON.stringify(item)}`
}
})
console.result += '\n\n'
}

// 沙盒日志
const console: { result: string, log: Function, warn: Function, error: Function } = {
result: '',
log: (msg: any, ...args: any[]) => {
globalConsole.log('Sandbox console', msg, ...args)
sandboxConsole('debug', msg, ...args)
}
}

// vm 主要封装
// sandbox:any 是提供给前端允许调用的模块
// code 需要执行的代码
export default async function executeCodeInSandbox(sandbox: any, code?: string) {
if (!code || code.trim().length === 0) {
throw Error('Nothing executed.')
}

// 结构化代码
const slices = code.trim().split(/\r?\n/)
.map((l: string, i: number) => {
return l.trimEnd()
})
.filter((l: string, i: number) => {
return /^[A-Za-z0-9{}(&|]/.test(l.trimStart())
})
if (slices.length == 0) {
throw Error('Nothing executed.')
} else if (slices.length == 1) {
code = slices[0]
} else {
code = slices.reduce((p: string, c: string, i: number, a: string[]) => {
return `${p}\n ${c}`
})
}

// 对要调用的代码做一层安全包装,同时在执行前打开 DB,以便支持数据库查询操作
let wrapCode = `globalConsole.log('VM exec in process:', process.pid, process.title)
try {
console.result = ''
await connectDBAsync()
${code}
await disconnectDBAsync()
inject(console.result)
} catch (e) {
inject(e)
}`

console.log(wrapCode);

// 在 node:vm 中执行 code
await (async (code: string) => {
return new Promise((resolve) => {
const script: Script = new vm.Script(`(async()=>{${code}})()`)
const options: RunningScriptOptions = {
timeout: 1000,
displayErrors: true,
}
const context: Context = vm.createContext({
...sandbox,
console,
globalConsole,
process,
connectDBAsync,
disconnectDBAsync,
inject: (result: any) => {
sandbox.result = result
resolve(result)
}
})
script.runInContext(context, options)
})
})(wrapCode)

// 返回代码执行结果
return sandbox.result // util.inspect(sandbox.result)
}

Koa 路由接收 http 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import Router from 'koa-router'
import Koa from "koa"
import executeCodeInSandbox from "../utils/sandbox";
import { getUserByEmail } from '../db/userDao';


// modules which register in sandboxEnv, the code can reference only !!!
const sandboxEnv: any = {
require,
getUserByEmail
}

export const sandboxRouter: Router = new Router()

sandboxRouter.post('execute-code', '/execute-code', async (ctx: Koa.Context) => {
const { codeData } = ctx.request.body as { codeData?: string }
let result: any
console.log('sandbox exec start...\n', codeData)
try {
result = await executeCodeInSandbox({ ...sandboxEnv }, codeData)
if (typeof result !== 'object') {
result = `${result}`
}
} catch (e: any) {
result = e.stack ? e.stack : `${e}`
console.error('sandbox exec error', result)
}
console.log('sandbox exec end...\n', result)
ctx.body = {
result: result
}
})

这里需要注意, server 端可以将 sandboxEnv(utils/daos 等模块) 注册到了沙盒 vm context 中,只有注册到沙盒中的模块,前端代码才能够调用的到!

在前端代码编辑器中编写代码查看运行效果:

figure_1