浏览器端沙箱
问题😭
在服务端,除了要对网络请求进行拦截,限制访问全局变量(process、require等),还要拦截访问文件资源,限制cpu和内存的使用等。
解决方案
1、vm2💞
vm2主要用于创建一个安全的沙箱环境,基于node内置模块vm实现,防止恶意代码访问主进程的资源。
使用
1、我们可以通过new VM来创建vm实例,然后vm.run实例方法执行脚本。
2、在创建vm实例时,我们可以通过sandbox属性重写一些全局方法(require、process等),从而进行拦截
3、我们可以通过process.cpuUsage()和process.memoryUsage()来监控cpu和内存的使用情况。
const { VM } = require('vm2')
const fs = require('fs')
const os = require('os')
function runInSandbox(code, timeLimit = 1000, memoryLimit = 50 * 1024 * 1024) {
const vm = new VM({
timeout: timeLimit,
sandbox: {
// 选择性暴露某些API
console: console,
require: (moduule) => {
return require(moduule)
},
readFile: (path) => {
// 可以在这里添加额外的安全检查
return fs.readFileSync(path, 'utf8')
},
makeRequest: (url) => {
// 自定义的网络请求函数,可以在这里实现拦截逻辑
console.log(`请求被拦截:${url}`)
return '模拟的响应数据'
}
}
})
const startCPU = process.cpuUsage()
const startMem = process.memoryUsage()
let intervalId
try {
intervalId = setInterval(() => {
const currentMem = process.memoryUsage()
if (currentMem.heapUsed - startMem.heapUsed > memoryLimit) {
clearInterval(intervalId)
throw new Error('内存使用超出限制')
}
}, 100)
const result = vm.run(code)
clearInterval(intervalId)
const endCPU = process.cpuUsage(startCPU)
const endMem = process.memoryUsage()
console.log('CPU使用 (微秒):', endCPU.user + endCPU.system)
console.log('内存使用 (字节):', endMem.heapUsed - startMem.heapUsed)
return result
} catch (err) {
clearInterval(intervalId)
console.error('执行错误:', err)
}
}
// 使用示例
const code = `
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += i;
}
sum;
`
console.log('执行结果:', runInSandbox(code))
2、isolated-vm💕
vm2的官网推荐用ivm来构建沙箱,更安全吧。
特点:
1、isolated-vm提供了完全隔离的v8实例,每个实例有独立的内存和上下文。
2、支持不同实例之间通信。
3、提供更详细的控制,可以精确控制内存和资源的使用。
4、比vm2更安全。【主要vm2不维护了,也推荐ivm,ivm是更安全吧,但是用起来很麻烦....😭】
使用
1、内存限制可以在new ivm实例时直接设置,而不用手动来监听。
2、宿主给沙箱的变量、函数等,需要通过new ivm.Reference(a)包裹处理。
3、宿主函数的返回值为引用类型时,需要new ivm.ExternalCopy(result).copyInto()进行处理。
4、context.global.set()用于设置沙箱环境的全局变量,如果一个全局函数是返回promise,需要借用applySyncPromise来调用才能保证顺序。
具体代码如下:
const ivm = require('isolated-vm')
const axios = require('axios')
;(async () => {
const isolate = new ivm.Isolate({ memoryLimit: 128 })
const context = await isolate.createContext()
const jail = context.global
await jail.set('global', jail.derefInto())
// 创建一个宿主函数
const hostFunction = async function (url, option = { method: 'get' }) {
try {
const response = await axios.request({ url, ...option })
console.log(response.data, '宿主函数的结果')
return response.data
} catch (error) {
console.error('宿主函数错误:', error.message)
return { error: error.message }
}
}
// 创建一个包装函数
const wrapperFunction = new ivm.Reference(async function (...args) {
const result = await hostFunction.apply(undefined, args)
return new ivm.ExternalCopy(result).copyInto() //引用类型的结果这样处理
})
await jail.set('hostFunctionWrapper', wrapperFunction)
// 在沙箱中定义一个普通函数来调用包装函数
await context.evalClosure(
`
global.hostFunction = async function(...args) {
try {
const res = await hostFunctionWrapper.applySyncPromise(undefined, args);
//promise类的需要用applySyncPromise调用,其他的可以用apply
return res;
} catch(err) {
console.log(err, '沙箱错误');
throw err;
}
};
`,
[wrapperFunction] //可以传递空数组,除非要用$0
)
// 拦截console
const consoleFn = new ivm.Reference(function (...args) {
console.log(...args.map((arg) => (typeof arg === 'string' ? arg : JSON.stringify(arg))))
})
await jail.set('consoleFn', consoleFn)
await context.evalClosure(`
global.console = {
log: function(...args) {
return consoleFn.apply(undefined, args);
}
};
`)
const userCode = `
(async function(){
console.log('开始执行沙箱代码');
try {
let res = await hostFunction('http://127.0.0.1:3000/getData');
console.log(res.msg, '请求结果');
} catch(err) {
console.log(err, '请求失败');
}
console.log('沙箱代码执行完毕');
})();
`
try {
await context.eval(userCode)
// 等待异步操作完成
await new Promise((resolve) => setTimeout(resolve, 1000))
} catch (err) {
console.error('Error executing script:', err)
}
// 释放引用
wrapperFunction.release()
consoleFn.release()
})()
END🤣
1、如果觉得ivm用起来麻烦,就用vm2。
2、出了做沙箱,更应该对脚本进行日志记录😚