JS沙箱sandbox的各种实现

Favori,

Sandbox

图:Peter Tarka

with 全代理沙箱

// 监控执行代码
function compileCode(src) {
  src = `with (exposeObj){${src}}`;
  return new Function("exposeObj", src);
}
// 代理对象
function proxyObj(originObj) {
  let exposeObj = new Proxy(originObj, {
    has: (target, key) => {
      if (["console", "Math", "Date"].indexOf(key) >= 0) {
        return target[key];
      }
      if (!target.hasOwnProperty(key)) {
        throw new Error(`${target}上不存在${key}`);
      }
      return target[key];
    },
  });
  return exposeObj;
}
// 创建沙盒
function createSandbox(src, obj) {
  let proxy = proxyObj(obj);
  compileCode(src).call(proxy, proxy);
}
 
const testObj = {
  value: 1,
  a: {
    b: 2,
  },
  c: "3",
};
const c = "c";
createSandbox(`value='32323';console.log(c);`, testObj);

快照 Snapshot 沙箱

核心原理是激活当前沙箱时把前宿主环境的全局变量备份一下,并把上一次对该沙箱做的更改恢复一下(若存在)

失活时找出当次沙箱和备份的全局变量不同的属性,存储一下,并把存储的宿主环境恢复一下

哈,双缓存策略

class SnapshotSandBox {
  constructor(name) {
    this.modifyMap = {}; // 存放修改的属性
    this.windowSnapshot = {};
  }
  active() {
    // 缓存active状态的沙箱
    this.windowSnapshot = {};
    for (const item in window) {
      this.windowSnapshot[item] = window[item];
    }
 
    Object.keys(this.modifyMap).forEach((p) => {
      window[p] = this.modifyMap[p];
    });
  }
  inactive() {
    for (const item in window) {
      if (this.windowSnapshot[item] !== window[item]) {
        // 记录变更
        this.modifyMap[item] = window[item];
        // 还原window
        window[item] = this.windowSnapshot[item];
      }
    }
  }
}
window.a = "1";
const diffSandbox = new SnapshotSandBox();
diffSandbox.active(); // 激活沙箱
debugger;
window.a = "0";
console.log("开启沙箱:", window.a);
diffSandbox.inactive(); //失活沙箱
debugger;
console.log("失活沙箱:", window.a);
diffSandbox.active(); // 重新激活
debugger;
console.log("再次激活", window.a);

这种方式也无法支持多实例,因为运行期间所有的属性都是保存在 window 上的。

代理 Proxy 沙箱

单实例

class LegacySandBox {
  addedPropsMapInSandbox = new Map();
  modifiedPropsOriginalValueMapInSandbox = new Map();
  currentUpdatedPropsValueMap = new Map();
  proxyWindow;
  setWindowProp(prop, value, toDelete = false) {
    if (value === undefined && toDelete) {
      delete window[prop];
    } else {
      window[prop] = value;
    }
  }
  active() {
    this.currentUpdatedPropsValueMap.forEach((value, prop) =>
      this.setWindowProp(prop, value)
    );
  }
  inactive() {
    this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop) =>
      this.setWindowProp(prop, value)
    );
    this.addedPropsMapInSandbox.forEach((_, prop) =>
      this.setWindowProp(prop, undefined, true)
    );
  }
  constructor() {
    const fakeWindow = Object.create(null);
    this.proxyWindow = new Proxy(fakeWindow, {
      set: (target, prop, value, receiver) => {
        const originalVal = window[prop];
        if (!window.hasOwnProperty(prop)) {
          this.addedPropsMapInSandbox.set(prop, value);
        } else if (!this.modifiedPropsOriginalValueMapInSandbox.has(prop)) {
          this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal);
        }
        this.currentUpdatedPropsValueMap.set(prop, value);
        window[prop] = value;
      },
      get: (target, prop, receiver) => {
        return target[prop];
      },
    });
  }
}
// 验证:
let legacySandBox = new LegacySandBox();
legacySandBox.active();
legacySandBox.proxyWindow.city = "Beijing";
console.log("window.city-01:", window.city);
legacySandBox.inactive();
console.log("window.city-02:", window.city);
legacySandBox.active();
console.log("window.city-03:", window.city);
legacySandBox.inactive();
// 输出:
// window.city-01: Beijing
// window.city-02: undefined
// window.city-03: Beijing

多实例

class MultipleProxySandbox {
  active() {
    this.sandboxRunning = true;
  }
  inactive() {
    this.sandboxRunning = false;
  }
  constructor() {
    const rawWindow = window;
    const fakeWindow = {};
    const proxy = new Proxy(fakeWindow, {
      set: (target, prop, value) => {
        if (this.sandboxRunning) {
          target[prop] = value;
          return true;
        }
      },
      get: (target, prop) => {
        // 如果fakeWindow里面有,就从fakeWindow里面取,否则,就从外部的window里面取
        let value = prop in target ? target[prop] : rawWindow[prop];
        return value;
      },
    });
    this.proxy = proxy;
  }
}
 
const context = { document: window.document };
 
const newSandBox1 = new MultipleProxySandbox("代理沙箱1", context);
newSandBox1.active();
const proxyWindow1 = newSandBox1.proxy;
 
const newSandBox2 = new MultipleProxySandbox("代理沙箱2", context);
newSandBox2.active();
const proxyWindow2 = newSandBox2.proxy;
console.log(
  "共享对象是否相等",
  window.document === proxyWindow1.document,
  window.document === proxyWindow2.document
);
 
proxyWindow1.a = "1"; // 设置代理1的值
proxyWindow2.a = "2"; // 设置代理2的值
window.a = "3"; // 设置window的值
console.log("打印输出的值", proxyWindow1.a, proxyWindow2.a, window.a);
 
newSandBox1.inactive();
newSandBox2.inactive(); // 两个沙箱都失活
 
proxyWindow1.a = "4"; // 设置代理1的值
proxyWindow2.a = "4"; // 设置代理2的值
window.a = "4"; // 设置window的值
console.log("失活后打印输出的值", proxyWindow1.a, proxyWindow2.a, window.a);
 
newSandBox1.active();
newSandBox2.active(); // 再次激活
 
proxyWindow1.a = "4"; // 设置代理1的值
proxyWindow2.a = "4"; // 设置代理2的值
window.a = "4"; // 设置window的值
console.log("失活后打印输出的值", proxyWindow1.a, proxyWindow2.a, window.a);

可以看出最后一种实现是最优实现,既没有操作 window,又能实现多实例,代码又精简,通俗易懂 👍🏻