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 产生影响;

  1. // 简化伪代码示例
  2. window = new Proxy(pick(window, whiteListProperties), { ... })
  3. document = new Proxy(document, { ... })
  4. ...
  5. sandbox = new Function(`
  6. return function ({ window, location, history, document }, code){
  7. with(window) {
  8. ${code}
  9. }
  10. }`)
  11. 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 拦截做到了 「深拷贝」,是一种目前比较完善优雅的沙箱方案;

  1. // 简化伪代码示例
  2. frame = document.body.appendChild(document.createElement('iframe',{
  3. src: 'about:blank',
  4. sandbox: "allow-scripts allow-same-origin allow-popups allow-presentation allow-top-navigation",
  5. style: 'display: none;',
  6. }))
  7. window = new Proxy(frame.contentWindow, { ... })
  8. document = new Proxy(document, { ... })
  9. ...
  10. sandbox = new Function(`
  11. return function ({ window, location, history, document }, code){
  12. with(window) {
  13. ${code}
  14. }
  15. }`)
  16. sandbox().call(window, { window, location, history, document }, code)