petite-vue源码剖析-沙箱模型
在解析v-if和v-for等指令时我们会看到通过evaluate执行指令值中的JavaScript表达式,而且能够读取当前作用域上的属性。而evaluate的实现如下:
const evalCache: Record<string, Function> = Object.create(null)
export const evaluate = (scope: any, exp: string, el?: Node) =>
execute(scope, `return(${exp})`, el)
export const execute = (scope: any, exp: string, el?: Node) => {
const fn = evalCache[exp] || (evalCache[exp] = toFunction(exp))
try {
return fn(scope, el)
} catch (e) {
if (import.meta.env.DEV) {
console.warn(`Error when evaluating expression "${exp}":`)
}
console.error(e)
}
}
const toFunction = (exp: string): Function => {
try {
return new Function(`$data`, `$el`, `with($data){${exp}}`)
} catch (e) {
console.error(`${(e as Error).message} in expression: ${exp}`)
return () => {}
}
}
简化为如下
export const evaluate = (scope: any, exp: string, el?: Node) => {
return (new Function(`$data`, `$el`, `with($data){return(${exp})}`))(scope, el)
}
而这里就是通过with+new Function构建一个简单的沙箱,为v-if和v-for指令提供一个可控的JavaScript表达式的执行环境。
什么是沙箱
沙箱(Sandbox)作为一种安全机制,用于提供一个独立的可控的执行环境供未经测试或不受信任的程序运行,并且程序运行不会影响污染外部程序的执行环境(如篡改/劫持window对象及其属性),也不会影响外部程序的运行。
与此同时,沙箱和外部程序可以通过预期的方式进行通信。
更细化的功能就是:
- 拥有独立的全局作用域和全局对象(
window) - 沙箱提供启动、暂停、恢复和停机功能
- 多台沙箱支持并行运行
- 沙箱和主环境、沙箱和沙箱之间可实现安全通信
原生沙箱-iframe
iframe拥有独立的browser context,不单单提供独立的JavaScript执行环境,甚至还拥有独立的HTML和CSS命名空间。
通过将iframe的src设置为about:blank即保证同源且不会发生资源加载,那么就可以通过iframe.contentWindow获取与主环境独立的window对象作为沙箱的全局对象,并通过with将全局对象转换为全局作用域。
而iframe的缺点:
- 若我们只需要一个独立的JavaScript执行环境,那么其它特性则不仅仅是累赘,还会带来不必要的性能开销。而且
iframe会导致主视窗的onload事件延迟执行; - 内部程序可以访问浏览器所有API,我们无法控制白名单。(这个可以通过Proxy处理)
沙箱的材料-with+Proxy+eval/new Function
什么是with?
JavaScript采用的是语法作用域(或称为静态作用域),而with则让JavaScript拥有部分动态作用域的特性。
with(obj)会将obj对象作为新的临时作用域添加到当前作用域链的顶端,那么obj的属性将作为当前作用域的绑定,但是和普通的绑定解析一样,若在当前作用域无法解析则会向父作用域查找,直到根作用域也无法解析为止。
let foo = 'lexical scope'
let bar = 'lexical scope'
;(function() {
// 访问语句源码书写的位置决定这里访问的foo指向'lexical scope'
console.log(foo)
})()
// 回显 lexical scope
;(function(dynamicScope) {
with(dynamicScope) {
/**
* 默认访问语句源码书写的位置决定这里访问的foo指向'lexical scope',
* 但由于该语句位于with的语句体中,因此将改变解析foo绑定的作用域。
*/
console.log(foo)
// 由于with创建的临时作用域中没有定义bar,因此会向父作用域查找解析绑定
console.log(bar)
}
})({
foo: 'dynamic scope'
})
// 回显 dynamic scope
// 回显 lexical scope
注意:with创建的是临时作用域,和通过函数创建的作用域是不同的。具体表现为当with中调用外部定义的函数,那么在函数体内访问绑定时,由于由with创建的临时作用域将被函数作用域替代,而不是作为函数作用域的父作用域而存在,导致无法访问with创建的作用域中的绑定。这也是为何说with让JavaScript拥有部分动态作用域特性的原因了。
let foo = 'lexical scope'
function showFoo() {
console.log(foo)
}
;(function(dynamicScope) {
with(dynamicScope) {
showFoo()
}
})({
foo: 'dynamic scope'
})
// 回显 lexical scope
再一次注意:若函数是在with创建的临时作用域内定义的,那么将以该临时作用域作为父作用域
let foo = 'lexical scope'
;(function(dynamicScope) {
with(dynamicScope) {
(() => {
const bar = 'bar'
console.log(bar)
// 其实这里就是采用语法作用域,谁叫函数定义的位置在临时作用域生效的地方呢。
console.log(foo)
})()
}
})({
foo: 'dynamic scope'
})
// 回显 bar
// 回显 dynamic scope
另外,在ESM模式或strict模式(使用class定义类会启动启用strict模式)下都禁止使用with语句哦!
Error: With statements cannot be used in an ECMAScript moduleUncaught SyntaxError: Strict mode code may not include a with statement
但无法阻止通过eval或new Function执行with哦!
如何利用Proxy防止绑定解析逃逸?
通过前面数篇文章的介绍,我想大家对Proxy已经不再陌生了。不过这里我们会用到之前一笔带过的has拦截器,用于拦截with代码中任意变量的访问,也可以设置一个可正常在作用域链查找的绑定白名单,而白名单外的则必须以沙箱创建的作用域上定义维护。
const whiteList = ['Math', 'Date', 'console']
const createContext = (ctx) => {
return new Proxy(ctx, {
has(target, key) {
// 由于代理对象作为`with`的参数成为当前作用域对象,因此若返回false则会继续往父作用域查找解析绑定
if (whiteList.includes(key)) {
return target.hasOwnProperty(key)
}
// 返回true则不会往父作用域继续查找解析绑定,但实际上没有对应的绑定,则会返回undefined,而不是报错,因此需要手动抛出异常。
if (!targe.hasOwnProperty(key)) {
throw ReferenceError(`${key} is not defined`)
}
return true
}
})
}
with(createContext({ foo: 'foo' })) {
console.log(foo)
console.log(bar)
}
// 回显 foo
// 抛出 `Uncaught ReferenceError: bar is not defined`
到目前为止,我们虽然实现一个基本可用沙箱模型,但致命的是无法将外部程序代码传递沙箱中执行。下面我们通过eval和new Function来实现。
邪恶的eval
eval()函数可以执行字符串形式的JavaScript代码,其中代码可以访问闭包作用域及其父作用域直到全局作用域绑定,这会引起代码注入(code injection)的安全问题。
const bar = 'bar'
function run(arg, script) {
;(() => {
const foo = 'foo'
eval(script)
})()
}
const script = `
console.log(arg)
console.log(bar)
console.log(foo)
`
run('hi', script)
// 回显 hi
// 回显 bar
// 回显 foo
new Function
相对eval,new Function的特点是:
new Funciton函数体中的代码只能访问函数入参和全局作用域的绑定;- 将动态脚本程序解析并实例化为函数对象,后续不用再重新解析就可以至直接执行,性能比
eval好。
const bar = 'bar'
function run(arg, script) {
;(() => {
const foo = 'foo'
;(new Function('arg', script))(arg)
})()
}
const script = `
console.log(arg)
console.log(bar)
console.log(foo)
`
run('hi', script)
// 回显 hi
// 回显 bar
// 回显 Uncaught ReferenceError: foo is not defined
沙箱逃逸(Sandbox Escape)
沙箱逃逸就是沙箱内运行的程序以非合法的方式访问或修改外部程序的执行环境或影响外部程序的正常执行。
虽然上面我们已经通过Proxy控制沙箱内部程序可访问的作用域链,但仍然有不少突破沙箱的漏洞。
通过原型链实现逃逸
JavaScript中constructor属性指向创建当前对象的构造函数,而该属性是存在于原型中,并且是不可靠的。
function Test(){}
const obj = new Test()
console.log(obj.hasOwnProperty('constructor')) // false
console.log(obj.__proto__.hasOwnProperty('constructor')) // true
逃逸示例:
// 在沙箱内执行如下代码
({}).constructor.prototype.toString = () => {
console.log('Escape!')
}
// 外部程序执行环境被污染了
console.log(({}).toString())
// 回显 Escape!
// 而期待回显是 [object Object]
Symbol.unscopables
Symbol.unscopables作为属性名对应的属性值表示该对象作为with参数时,哪些属性会被with环境排除。
const arr = [1]
console.log(arr[Symbol.unscopables])
// 回显 {"copyWithin":true,"entries":true,"fill":true,"find":true,"findIndex":true,"flat":true,"flatMap":true,"includes":true,"keys":true,"values":true,"at":true,"findLast":true,"findLastIndex":true}
with(arr) {
console.log(entries) // 抛出ReferenceError
}
const includes = '成功逃逸啦'
with(arr) {
console.log(includes) // 回显 成功逃逸啦
}
防范的方法就是通过Proxy的get拦截器,当访问Symbol.unscopables时返回undefined
const createContext = (ctx) => {
return new Proxy(ctx, {
has(target, key) {
// 由于代理对象作为`with`的参数成为当前作用域对象,因此若返回false则会继续往父作用域查找解析绑定
if (whiteList.includes(key)) {
return target.hasOwnProperty(key)
}
// 返回true则不会往父作用域继续查找解析绑定,但实际上没有对应的绑定,则会返回undefined,而不是报错,因此需要手动抛出异常。
if (!targe.hasOwnProperty(key)) {
throw ReferenceError(`${key} is not defined`)
}
return true
},
get(target, key, receiver) {
if (key === Symbol.unscopables) {
return undefined
}
return Reflect.get(target, key, receiver)
}
})
}
实现一个基本安全的沙箱
const toFunction = (script: string): Function => {
try {
return new Function('ctx', `with(ctx){${script}}`)
} catch (e) {
console.error(`${(e as Error).message} in script: ${script}`)
return () => {}
}
}
const toProxy = (ctx: object, whiteList: string[]) => {
return new Proxy(ctx, {
has(target, key) {
// 由于代理对象作为`with`的参数成为当前作用域对象,因此若返回false则会继续往父作用域查找解析绑定
if (whiteList.includes(key)) {
return target.hasOwnProperty(key)
}
// 返回true则不会往父作用域继续查找解析绑定,但实际上没有对应的绑定,则会返回undefined,而不是报错,因此需要手动抛出异常。
if (!targe.hasOwnProperty(key)) {
throw ReferenceError(`${key} is not defined`)
}
return true
},
get(target, key, receiver) {
if (key === Symbol.unscopables) {
return undefined
}
return Reflect.get(target, key, receiver)
}
})
}
class Sandbox {
private evalCache: Map<string, Function>
private ctxCache: WeakMap<object, Proxy>
constructor(private whiteList: string[] = ['Math', 'Date', 'console']) {
this.evalCache = new Map<string, Function>()
this.ctxCache = new WeakMap<object, Proxy>()
}
run(script: string, ctx: object) {
if (!this.evalCache.has(script)) {
this.evalCache.set(script, toFunction(script))
}
const fn = this.evalCache.get(script)
if (!this.ctxCache.has(ctx)) {
this.ctxCache.set(ctx, toProxy(ctx, this.whiteList))
}
const ctxProxy = this.ctxCache.get(ctx)
return fn(ctx)
}
到此我们已经实现一个基本安全的沙箱模型,但远远还没达到生产环境使用的要求。
总结
上述我们是通过Proxy阻止沙箱内的程序访问全局作用域的内容,若没有Proxy那么要怎样处理呢?另外,如何实现沙箱的启停、恢复和并行运行呢?其实这个我们可以看看蚂蚁金服的微前端框架qiankun(乾坤)是如何实现的,具体内容请期待后续的《微前端框架qiankun源码剖析》吧!
尊重原创,转载请注明来自:https://www.cnblogs.com/fsjohnhuang/p/16169903.html 肥仔John
petite-vue源码剖析-沙箱模型的更多相关文章
- 一个Python开源项目-腾讯哈勃沙箱源码剖析(上)
前言 2019年来了,2020年还会远吗? 请把下一年的年终奖发一下,谢谢... 回顾逝去的2018年,最大的改变是从一名学生变成了一位工作者,不敢说自己多么的职业化,但是正在努力往那个方向走. 以前 ...
- petite-vue源码剖析-逐行解读@vue/reactivity之reactive
在petite-vue中我们通过reactive构建上下文对象,并将根据状态渲染UI的逻辑作为入参传递给effect,然后神奇的事情发生了,当状态发生变化时将自动触发UI重新渲染.那么到底这是怎么做到 ...
- 逐行剖析Vue源码(一)——写在最前面
1. 前言 博主作为一名前端开发,日常开发的技术栈是Vue,并且用Vue开发也有一年多了,对其用法也较为熟练了,但是对各种用法和各种api使用都是只知其然而不知其所以然,因此,有时候在排查bug的时候 ...
- Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现
声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...
- Apache Spark源码剖析
Apache Spark源码剖析(全面系统介绍Spark源码,提供分析源码的实用技巧和合理的阅读顺序,充分了解Spark的设计思想和运行机理) 许鹏 著 ISBN 978-7-121-25420- ...
- 基于mybatis-generator-core 1.3.5项目的修订版以及源码剖析
项目简单说明 mybatis-generator,是根据数据库表.字段反向生成实体类等代码文件.我在国庆时候,没事剖析了mybatis-generator-core源码,写了相当详细的中文注释,可以去 ...
- SpringMVC源码剖析(二)- DispatcherServlet的前世今生
上一篇文章<SpringMVC源码剖析(一)- 从抽象和接口说起>中,我介绍了一次典型的SpringMVC请求处理过程中,相继粉墨登场的各种核心类和接口.我刻意忽略了源码中的处理细节,只列 ...
- socket_server源码剖析、python作用域、IO多路复用
本节内容: 课前准备知识: 函数嵌套函数的使用方法: 我们在使用函数嵌套函数的时候,是学习装饰器的时候,出现过,由一个函数返回值是一个函数体情况. 我们在使用函数嵌套函数的时候,最好也这么写. def ...
- 《Apache Spark源码剖析》
Spark Contributor,Databricks工程师连城,华为大数据平台开发部部长陈亮,网易杭州研究院副院长汪源,TalkingData首席数据科学家张夏天联袂力荐1.本书全面.系统地介绍了 ...
随机推荐
- UVA1389 Hard Life (01分数规划+最大流)
UVA1389 Hard Life (01分数规划+最大流) Luogu 题目描述略 题解时间 $ (\frac{\Sigma EdgeCount}{\Sigma PointCount})_{max} ...
- 羽夏看Win系统内核——结语
写在前面 此系列是本人一个字一个字码出来的,包括示例和实验截图.由于系统内核的复杂性,故可能有错误或者不全面的地方,如有错误,欢迎批评指正,本教程将会长期更新. 如有好的建议,欢迎反馈.码字不易, ...
- 什么是MVC模式?
MVC (Model View Controller) 是一个设计模式,使用MVC应用程序被分成三个核心部件:模型.视图.控制器.它们各自处理自己的任务.M是指数据模型,V是指用户界面,C则是控制器. ...
- JdbcTemplate?
JdbcTemplate 类提供了很多便利的方法解决诸如把数据库数据转变成基本数据类型或对象,执行写好的或可调用的数据库操作语句,提供自定义的数据错误处理.
- Auth0案例学习(SpringMVC形式)
官方SpringMVC示例Github:https://github.com/auth0-samples/auth0-spring-mvc-sample/tree/master/01-Login Ma ...
- 学习Nginx(一)
实验目的 通过nginx实现反向代理的功能,类似apache反向代理和haproxy反向代理 工作中用nginx做反向代理和负载均衡的也越来越多了 有些公司从web服务器到反向代理,都使用nginx. ...
- centos 7环境下安装jdk
在此之前已经安装了xshell并能传输文件. 1.下载对应版本的jdk:jdk-8u191-linux-x64.tar.gz 2.上传到centos的目录下 3.解压jdk的gz包,命令: tar - ...
- jdk_8接口的内部内容
目标: 如何创建已定义好的接口类型的对象呢? 步骤: 实现的概述 抽象方法的使用 默认方法的使用 静态方法的使用 接口的常量使用 讲解: 实现的概述 类与接口的关系为实现关系,即类实现接口,该类可以称 ...
- 聊一聊Java8 Optional,让你的代码更加优雅
码农在囧途 随着时间的推移,曾经我们觉得重要的东西,可能在今天看来是如此的浅薄和无知,同理,今天我们放不下,想不开,觉得重要的东西,多年后我们可能也会觉得也就那样,所以,今天的的所有烦恼,忧愁,想不开 ...
- 4.2 ROS节点运行管理launch文件
4.2 ROS节点运行管理launch文件 关于 launch 文件的使用我们已经不陌生了,在第一章内容中,就曾经介绍到: 一个程序中可能需要启动多个节点,比如:ROS 内置的小乌龟案例,如果要控制乌 ...