When parsing instructions such as v-if
and v-for
we will see that the JavaScript expression in the instruction value is executed by evaluate
and can read the current scope Attributes. And the implementation of evaluate
is as follows:
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 () => {}
}
}
Simplified as follows
export const evaluate = (scope: any, exp: string, el?: Node) => {
return (new Function(`$data`, `$el`, `with($data){return(${exp})}`))(scope, el)
}
And here is to build a simple sandbox by with
+ new Function
to provide a controllable JavaScript expression for v-if
and v-for
a controllable JavaScript expression style execution environment.
What is a sandbox
Sandbox, as a security mechanism, is used to provide an independent and controllable execution environment for untested or untrusted programs to run, and the program operation will not affect the execution environment of polluting external programs (such as tampering/ hijacking the window object and its properties), and will not affect the operation of external programs.
At the same time, the sandbox and external programs can communicate in the expected way.
A more refined function is:
- Has separate global scope and global object (
window
) - Sandbox provides start, pause, resume and shutdown capabilities
- Multiple sandboxes support parallel operation
- Secure communication between sandbox and main environment, sandbox and sandbox
Native Sandbox iframe
iframe
Has an independent browser context, not only provides an independent JavaScript execution environment, but also has independent HTML and CSS namespaces.
By setting the iframe
of src
to --- iframe.contentWindow
about:blank
that is to ensure that the same origin and no resource loading will occur, then you can obtain the The main environment independent window object is used as the global object of the sandbox, and the global object is converted to the global scope by with
.
And the disadvantages of iframe
:
- If we only need a separate JavaScript execution environment, then other features are not only cumbersome, but also bring unnecessary performance overhead. And
iframe
will cause the onload event of the main window to delay execution; - Internal programs can access all browser APIs, we have no control over the whitelist. (This can be handled by Proxy)
Sandbox material with
+ Proxy
+ eval/new Function
What is with
?
JavaScript uses syntactic scoping (or static scoping), and with
gives JavaScript some of the features of dynamic scoping.
with(obj)
will add the obj
object as a new temporary scope to the top of the current scope chain, then the properties of obj
will be used as bindings for the current scope , but like ordinary binding resolution, if it cannot be resolved in the current scope, it will look to the parent scope until the root scope cannot be resolved.
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
Note: with
creates a temporary scope, which is different from a scope created by a function. The specific performance is that when an externally defined function is called in with
, when the binding is accessed in the function body, the temporary scope created by with
will be replaced by the function scope instead of Exists as a parent scope of a function scope, resulting in inaccessibility of bindings in scopes created by with
. That's why with
gives JavaScript some of the dynamic scoping features.
let foo = 'lexical scope'
function showFoo() {
console.log(foo)
}
;(function(dynamicScope) {
with(dynamicScope) {
showFoo()
}
})({
foo: 'dynamic scope'
})
// 回显 lexical scope
Note again: if the function is defined in a temporary scope created by with
, then the temporary scope will be used as the parent scope
let foo = 'lexical scope'
;(function(dynamicScope) {
with(dynamicScope) {
(() => {
const bar = 'bar'
console.log(bar)
// 其实这里就是采用语法作用域,谁叫函数定义的位置在临时作用域生效的地方呢。
console.log(foo)
})()
}
})({
foo: 'dynamic scope'
})
// 回显 bar
// 回显 dynamic scope
In addition, it is forbidden to use the with
statement in ESM mode or strict mode (use class
define the class to enable strict mode )!
-
Error: With statements cannot be used in an ECMAScript module
-
Uncaught SyntaxError: Strict mode code may not include a with statement
But can't prevent the execution of ---f67be892045f1f0134296da155f6adc0--- through eval
or new Function
with
oh!
How to use Proxy
to prevent binding parsing escape?
Through the introduction of the previous articles, I think everyone is no longer unfamiliar with Proxy
. But here we will use the has
interceptor, which we have brought before, to intercept the access of any variable in the code of with
, and we can also set a binding that can be normally searched in the scope chain Whitelist, and those outside the whitelist must be defined and maintained in the scope created by the sandbox.
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`
So far, although we have implemented a basic usable sandbox model, the fatal thing is that the external program code cannot be passed to the sandbox for execution. Below we do it by eval
and new Function
.
Evil eval
eval()
Functions can execute JavaScript code in the form of strings, where the code can access the closure scope and its parent scope until the global scope is bound, which can cause code injection (code injection) security issues.
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
The relative characteristics of eval
and new Function
are:
-
new Funciton
The code in the function body can only access the binding of function input parameters and global scope ; - The dynamic script program is parsed and instantiated as a function object, and it can be directly executed without re-parsing later. The performance is better than
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
Sandbox escape means that the program running in the sandbox accesses or modifies the execution environment of the external program in an illegal way or affects the normal execution of the external program.
Although we have used Proxy to control the scope chain accessible to programs inside the sandbox, there are still many vulnerabilities that can break through the sandbox.
Escape via prototype chain
The constructor property in JavaScript points to the constructor that created the current object, and this property exists in the prototype and is unreliable.
function Test(){}
const obj = new Test()
console.log(obj.hasOwnProperty('constructor')) // false
console.log(obj.__proto__.hasOwnProperty('constructor')) // true
Escape example:
// 在沙箱内执行如下代码
({}).constructor.prototype.toString = () => {
console.log('Escape!')
}
// 外部程序执行环境被污染了
console.log(({}).toString())
// 回显 Escape!
// 而期待回显是 [object Object]
Symbol.unscopables
Symbol.unscopables
As the attribute value corresponding to the attribute name, it indicates which attributes will be excluded by the with environment when the object is used as the with
parameter.
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) // 回显 成功逃逸啦
}
The method of prevention is to pass the Proxy's get interceptor, and return undefined when accessing Symbol.unscopables
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)
}
})
}
Implement a basic security sandbox
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)
}
So far we have implemented a basic security sandbox model, but it is far from meeting the requirements for production environments.
Summarize
In the above, we use Proxy to prevent programs in the sandbox from accessing the content of the global scope. If there is no Proxy, what should we do? In addition, how to start, stop, resume and run the sandbox in parallel? In fact, we can see how Ant Financial's micro-front-end framework qiankun (Qiankun) is implemented. Please look forward to the follow-up "Micro-front-end framework qiankun Source Code Analysis" for details!
Respect originality, please indicate the source for reprint: https://www.cnblogs.com/fsjohnhuang/p/16169903.html Fat Boy John
"Anatomy of Petite-Vue Source Code" booklet
"Petite-Vue Source Code Analysis" combines examples to interpret the source code line by line from online rendering, responsive system and sandbox model, and also makes a detailed analysis of the SMI optimization dependency cleaning algorithm using the JS engine in the responsive system. It is definitely an excellent stepping stone before getting started with Vue3 source code. If you like it, remember to forward it and appreciate it!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。