with() + new Function(code) + Proxy
with
with 语法用于改变作用域链,这里用来拦截写访问全局变量时对 window 的查找,如直接访问 Array.from 而不是 window.Array.from 写法时;
new Function
new Function 执行 code 作用等同于 eval,但 eval 能访问到当前局部作用域变量,new Function 返回函数不管哪里执行,都只能访问全局作用域,正是我们想要的。
Proxy
而 Proxy 提供的是 with 和 new Function 闭包中用到的充当 window 作用域的对象,通过白名单属性限制能访问真正 window 上的部分元素,通过 Proxy 让删除 / 添加全局变量 / api 时不会对真正全局 window 产生影响;
// 简化伪代码示例
window = new Proxy(pick(window, whiteListProperties), { ... })
document = new Proxy(document, { ... })
...
sandbox = new Function(`
return function ({ window, location, history, document }, code){
with(window) {
${code}
}
}`)
sandbox().call(window, { window, location, history, document }, code)
但这里对 window 拦截的程度是有限的,甚至可以简单理解为「浅拷贝」而非「深拷贝」,通过全局通用 API 很容易做到逃逸而实现污染,比如直接改掉 Array.prototype.push 的行为;
with() + new Function(code) + Proxy + iframe contex
为了更安全的解决上面的 Proxy window 全局 API 逃逸问题,可以取一个 iframe 的 window 作为沙箱环境上下文的 window;
这里的 iframe 并不是直接作为沙箱来执行子应用代码,子应用依然执行在 with + new Function 中,这个 iframe 只是个创建出来的空的 same-origin iframe,唯一用途是取它的 iframe.contentWindow 对象传给子应用做 window;
因为 iframe 的严格隔离性,一切全局对象跟外层均没有任何关系(除了 parent),因此内外两个 ArrayArray.prototype 都不相同,等同于把上一个方案的 window 拦截做到了 「深拷贝」,是一种目前比较完善优雅的沙箱方案;
// 简化伪代码示例
frame = document.body.appendChild(document.createElement('iframe',{
src: 'about:blank',
sandbox: "allow-scripts allow-same-origin allow-popups allow-presentation allow-top-navigation",
style: 'display: none;',
}))
window = new Proxy(frame.contentWindow, { ... })
document = new Proxy(document, { ... })
...
sandbox = new Function(`
return function ({ window, location, history, document }, code){
with(window) {
${code}
}
}`)
sandbox().call(window, { window, location, history, document }, code)