什么是沙箱?
是一种安全防护机制
在计算机安全中,沙箱是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或者代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的执行
举个栗子
- 比如浏览器中每个标签页运行一个独立的网页,每个标签页之间互不影响,这个标签页就是一个沙箱
- 引入不知名的第三方库也需要制造一个沙箱来执行这些代码
- Vue模板表达式的计算也是硬性在一个沙盒之中,在模板字符串中的表达式只能获取部分全局对象
具体实现
沙箱的实现核心在于指定一套程序执行机制,在这个机制下沙箱内部程序的运行不会影响外部运行 js是词法作用域,在解析变量的时候会一层一层往上找,所以需要With来做“一面围墙”,再通过Proxy.has() 来捕获拦截那些没有查到的变量,并且阻断查询
With + Proxy
Proxy 可以代理一个对象,从而拦截并定义对象的基本操作。
Proxy 中的 get 和 set 方法只能拦截已存在于代理对象中的属性,对于代理对象中不存在的属性这两个钩子是无感知的。因此这里我们使用 Proxy.has() 来拦截 with 代码块中的任意变量的访问,并设置一个白名单,在白名单内的变量可以正常走作用域链的访问方式,不在白名单内的变量会继续判断是否存在沙箱自行维护的上下文对象中,存在则正常访问,不存在则直接报错。
由于 has 会拦截 with 代码块中所有的变量访问,而我们只是想监控被执行代码块中的程序,因此还需要转换一下手动执行代码的形式 :
// 构造一个 with 来包裹需要执行的代码,返回 with 代码块的一个函数实例
function withedYourCode(code) {
code = 'with(globalObj) {' + code + '}'
return new Function('globalObj', code)
}
// 可访问全局作用域的白名单列表
const access_white_list = ['Math', 'Date']
// 待执行程序
const code = `
Math.random()
location.href = 'xxx'
func(foo)
`
// 执行上下文对象
const ctx = {
func: variable => {
console.log(variable)
},
foo: 'foo'
}
// 执行上下文对象的代理对象
const ctxProxy = new Proxy(ctx, {
has: (target, prop) => {
// has 可以拦截 with 代码块中任意属性的访问
if (access_white_list.includes(prop)) {
// 在可访问的白名单内,可继续向上查找
return target.hasOwnProperty(prop)
}
if (!target.hasOwnProperty(prop)) {
throw new Error(`Invalid expression - ${
prop}! You can not do that!`)
}
return true
}
})
// 没那么简陋的沙箱
function littlePoorSandbox(code, ctx) {
withedYourCode(code).call(ctx, ctx) // 将 this 指向手动构造的全局代理对象
}
littlePoorSandbox(code, ctxProxy)
// Uncaught Error: Invalid expression - location! You can not do that!
到这一步,其实很多较为简单的场景就可以覆盖了(eg: Vue 的模板字符串),那如果想要实现 CodeSanbox 这样的 web 编辑器呢?在这样的编辑器中我们可以任意使用诸如 document、location 等全局变量且不会影响主页面。