前言

四面的时分被问到了这个问题,其时第一时刻没有反应过来,觉得这个需求好独特

面试官给了一些提示,我才理解这道标题的意思,终究答复的也是磕磕绊绊

后来花了一些时刻整理了下思路,那么怎么规划这样的环境呢?

终究完成

完成思路:

1)运用 iframe 创立沙箱,取出其间的原生浏览器大局目标作为沙箱的大局目标

2)设置一个黑名单,若拜访黑名单中的变量,则直接报错,完成阻挠阻隔的效果

3)在黑名单中增加 document 字段,来完成制止开发者操作 DOM

4)在黑名单中增加 XMLHttpRequest、fetch、WebSocket 字段,完成禁用原生的办法调用接口

5)若拜访当时大局目标中不存在的变量,则直接报错,完成禁用三方库调接口

6)终究还要阻拦对 window 目标的拜访,避免经过 window.document 来操作 DOM,避免沙箱逃逸

下面聊一聊,为何这样规划,以及中心会遇到什么问题

怎么制止开发者操作 DOM ?

在页面中,能够经过 document 目标来获取 HTML 元素,进行增修正查的 DOM 操作

怎么制止开发者操作 DOM,转化为怎么阻挠开发者获取 document 目标

1)传统思路

简略粗犷点,直接修正 window.document 的值,让开发者无法获取 document

// 将document设置为null
window.document = null;
// 设置无效,打印成果仍是document
console.log(window.document); 
// 删去document
delete window.document
// 删去无效,打印成果仍是document
console.log(window.document);

好吧,document 修正不了也删去不了

运用 Object.getOwnPropertyDescriptor 查看,会发现 window.document 的 configurable 特点为 false(不行配置的)

Object.getOwnPropertyDescriptor(window, 'document');
// {get: , set: undefined, enumerable: true, configurable: false}

configurable 决议了是否能够修正特点描述目标,也便是说,configurable为false时,value、writable、enumerable和configurable 都不能被修正,以及无法被删去

此路不通,推倒重来

2)有点巨大上的思路

既然 document 目标修正不了,那假如环境华夏本就没有 document 目标,是不是就能够完成该需求?

说到环境中没有 document 目标,Web Worker 直呼内行,我曾在《一文彻底了解Web Worker,十万、百万条数据都是弟弟》中聊过怎么运用 Web Worker,和对应的特性

而且 Web Worker 更狠,不光没有 document 目标,连 window 目标也没有

在worker线程中打印window

onmessage = function (e) {
  console.log(window);
  postMessage();
};

浏览器直接报错

阿里面试官:请规划一个不能操作DOM和调接口的环境

在 Web Worker 线程的运转环境中无法拜访 document 目标,这一条符合当时的需求,可是该环境中能获取 XMLHttpRequest目标,能够发送 ajax 恳求,不符合不能调接口的要求

此路仍是不通……

怎么制止开发者调接口 ?

常规调接口办法有:

1)原生办法:XMLHttpRequest、fetch、WebSocket、jsonp、form表单

2)三方完成:axios、jquery、request等众多开源库

禁用原生办法调接口的思路:

1)XMLHttpRequest、fetch、WebSocket 这几种状况,能够制止用户拜访这些目标

2)jsonp、form 这两种办法,需求创立script或form标签,仍然能够经过制止开发者操作DOM的办法解决,不需求独自处理

怎么禁用三方库调接口呢?

三方库很多,没办法悉数列出来,来进行逐个扫除

制止调接口的路如同也被封死了……

终究计划:沙箱(Sandbox)

经过上面的剖析,传统的思路确实解决不了当时的需求

阻挠开发者操作DOM和调接口,沙箱说:这个我熟啊,阻拦阻隔这类的活,我最拿手了

沙箱(Sandbox) 是一种安全机制,为运转中的程序供给阻隔环境,一般用于履行未经测验或不受信任的程序或代码,它会为待履行的程序创立一个独立的履行环境,内部程序的履行不会影响到外部程序的运转

前端沙箱的运用场景:

1)Chrome 浏览器翻开的每个页面便是一个沙箱,确保互相独立互不影响

2)履行 jsonp 恳求回来的字符串时或引入不知名第三方 JS 库时,或许需求发明一个沙箱来履行这些代码

3)Vue 模板表达式的计算是运转在一个沙箱中,模板字符串中的表达式只能获取部分大局目标,概况见源码

4)微前端结构 qiankun ,为了完成js阻隔,在多种场景下均运用了沙箱

沙箱的多种完成办法

先聊下 with 这个关键字:效果在于改变效果域,能够将某个目标增加到效果域链的顶部

with关于沙箱的意义:能够完成所有变量均来自牢靠或自主完成的上下文环境,而不会从大局的履行环境中取值,相当于做了一层阻拦,完成阻隔的效果

粗陋的沙箱

标题要求: 完成这样一个沙箱,要求程序中拜访的所有变量,均来自牢靠或自主完成的上下文环境,而不会从大局的履行环境中取值

举个: ctx作为履行上下文目标,待履行程序code能够拜访到的变量,有必要都来自ctx目标

// ctx 履行上下文目标
const ctx = {
  func: variable => {
    console.log(variable);
  },
  foo: "f1"
};
// 待履行程序
const code = `func(foo)`;

沙箱示例:

// 界说大局变量foo
var foo = "foo1";
// 履行上下文目标
const ctx = {
  func: variable => {
    console.log(variable);
  },
  foo: "f1"
};
// 非常粗陋的沙箱
function veryPoorSandbox(code, ctx) {
  // 运用with,将eval函数履行时的履行上下文指定为ctx
  with (ctx) {
    // eval能够将字符串按js代码履行,如eval('1+2')
    eval(code);
  }
}
// 待履行程序
const code = `func(foo)`;
veryPoorSandbox(code, ctx); 
// 打印成果:"f1",不是最外层的大局变量"foo1"

这个沙箱有一个显着的问题,若供给的ctx上下文目标中,没有找到某个变量时,代码仍会沿着效果域链一层层向上查找

假如上文示例中的 ctx 目标没有设置 foo特点,打印的成果仍是外层效果域的foo1

With + Proxy 完成沙箱

标题要求: 希望沙箱中的代码只在手动供给的上下文目标中查找变量,假如上下文目标中不存在该变量,则提示对应的错误

举个: ctx作为履行上下文目标,待履行程序code能够拜访到的变量,有必要都来自ctx目标,假如ctx目标中不存在该变量,直接报错,不再经过效果域链向上查找

完成步骤:

1)运用Proxy.has()来阻拦with代码块中的任意变量的拜访

2)设置一个白名单,在白名单内的变量能够正常走效果域链的拜访办法,不在白名单内的变量,会继续判断是否存 ctx 目标中,存在则正常拜访,不存在则直接报错

3)运用new Function替代eval,运用 new Function() 运转代码比eval更为好一些,函数的参数供给了清晰的接口来运转代码

new Function与eval的区别

沙箱示例:

var foo = "foo1";
// 履行上下文目标
const ctx = {
  func: variable => {
    console.log(variable);
  }
};
// 结构一个 with 来包裹需求履行的代码,回来 with 代码块的一个函数实例
function withedYourCode(code) {
  code = "with(shadow) {" + code + "}";
  return new Function("shadow", code);
}
// 可拜访大局效果域的白名单列表
const access_white_list = ["func"];
// 待履行程序
const code = `func(foo)`;
// 履行上下文目标的署理目标
const ctxProxy = new Proxy(ctx, {
  has: (target, prop) => {
    // has 能够阻拦 with 代码块中任意特点的拜访
    if (access_white_list.includes(prop)) {
      // 在可拜访的白名单内,可继续向上查找
      return target.hasOwnProperty(prop);
    }
    if (!target.hasOwnProperty(prop)) {
      throw new Error(`Not found - ${prop}!`);
    }
    return true;
  }
});
// 没那么粗陋的沙箱
function littlePoorSandbox(code, ctx) {
  // 将 this 指向手动结构的大局署理目标
  withedYourCode(code).call(ctx, ctx); 
}
littlePoorSandbox(code, ctxProxy);
// 履行func(foo),报错: Uncaught Error: Not found - foo!

履行成果:

阿里面试官:请规划一个不能操作DOM和调接口的环境

天然的优质沙箱(iframe)

iframe标签能够发明一个独立的浏览器原生等级的运转环境,这个环境由浏览器完成了与主环境的阻隔

运用 iframe 来完成一个沙箱是现在最便利、简略、安全的办法,能够把 iframe.contentWindow 作为沙箱履行的大局 window 目标

沙箱示例:

// 沙箱大局署理目标类
class SandboxGlobalProxy {
  constructor(sharedState) {
    // 创立一个 iframe 标签,取出其间的原生浏览器大局目标作为沙箱的大局目标
    const iframe = document.createElement("iframe", { url: "about:blank" });
    iframe.style.display = "none";
    document.body.appendChild(iframe);
    // sandboxGlobal作为沙箱运转时的大局目标
    const sandboxGlobal = iframe.contentWindow; 
    return new Proxy(sandboxGlobal, {
      has: (target, prop) => {
        // has 能够阻拦 with 代码块中任意特点的拜访
        if (sharedState.includes(prop)) {
          // 假如特点存在于同享的大局状态中,则让其沿着原型链在外层查找
          return false;
        }
        // 假如没有该特点,直接报错
        if (!target.hasOwnProperty(prop)) {
          throw new Error(`Not find: ${prop}!`);
        }
        // 特点存在,回来sandboxGlobal中的值
        return true;
      }
    });
  }
}
// 结构一个 with 来包裹需求履行的代码,回来 with 代码块的一个函数实例
function withedYourCode(code) {
  code = "with(sandbox) {" + code + "}";
  return new Function("sandbox", code);
}
function maybeAvailableSandbox(code, ctx) {
  withedYourCode(code).call(ctx, ctx);
}
// 要履行的代码
const code = `
  console.log(history == window.history) // false
  window.abc = 'sandbox'
  Object.prototype.toString = () => {
      console.log('Traped!')
  }
  console.log(window.abc) // sandbox
`;
// sharedGlobal作为与外部履行环境同享的大局目标
// code中获取的history为最外层效果域的history
const sharedGlobal = ["history"]; 
const globalProxy = new SandboxGlobalProxy(sharedGlobal);
maybeAvailableSandbox(code, globalProxy);
// 对外层的window目标没有影响
console.log(window.abc); // undefined
Object.prototype.toString(); // 并没有打印 Traped

能够看到,沙箱中对window的所有操作,都没有影响到外层的window,完成了阻隔的效果

需求完成

继续运用上述的 iframe标签来创立沙箱,代码主要修正点

1)设置 blacklist 黑名单,增加 document、XMLHttpRequest、fetch、WebSocket 来制止开发者操作DOM和调接口

2)判断要拜访的变量,是否在当时环境的 window 目标中,不在的直接报错,完成制止经过三方库调接口

// 设置黑名单
const blacklist = ['document', 'XMLHttpRequest', 'fetch', 'WebSocket'];
// 黑名单中的变量制止拜访
if (blacklist.includes(prop)) {
  throw new Error(`Can't use: ${prop}!`);
}

但有个很严重的缝隙,假如开发者经过 window.document 来获取 document 目标,仍然是能够操作 DOM 的

需求在黑名单中加入 window 字段,来解决这个沙箱逃逸的缝隙,尽管把 window 加入了黑名单,但 window 上的办法,如 open、close 等,仍然是能够正常获取运用的

终究代码:

// 沙箱大局署理目标类
class SandboxGlobalProxy {
  constructor(blacklist) {
    // 创立一个 iframe 标签,取出其间的原生浏览器大局目标作为沙箱的大局目标
    const iframe = document.createElement("iframe", { url: "about:blank" });
    iframe.style.display = "none";
    document.body.appendChild(iframe);
    // 获取当时HTMLIFrameElement的Window目标
    const sandboxGlobal = iframe.contentWindow;
    return new Proxy(sandboxGlobal, {
      // has 能够阻拦 with 代码块中任意特点的拜访
      has: (target, prop) => {
        // 黑名单中的变量制止拜访
        if (blacklist.includes(prop)) {
          throw new Error(`Can't use: ${prop}!`);
        }
        // sandboxGlobal目标上不存在的特点,直接报错,完成禁用三方库调接口
        if (!target.hasOwnProperty(prop)) {
          throw new Error(`Not find: ${prop}!`);
        }
        // 回来true,获取当时供给上下文目标中的变量;假如回来false,会继续向上层效果域链中查找
        return true;
      }
    });
  }
}
// 运用with关键字,来改变效果域
function withedYourCode(code) {
  code = "with(sandbox) {" + code + "}";
  return new Function("sandbox", code);
}
// 将指定的上下文目标,增加到待履行代码效果域的顶部
function makeSandbox(code, ctx) {
  withedYourCode(code).call(ctx, ctx);
}
// 待履行的代码code,获取document目标
const code = `console.log(document)`;
// 设置黑名单
// 经过小伙伴的指导,新增加Image字段,制止运用new Image来调接口
const blacklist = ['window', 'document', 'XMLHttpRequest', 'fetch', 'WebSocket', 'Image'];
// 将globalProxy目标,增加到新环境效果域链的顶部
const globalProxy = new SandboxGlobalProxy(blacklist);
makeSandbox(code, globalProxy);

打印成果:

阿里面试官:请规划一个不能操作DOM和调接口的环境

持续优化

经过与评论区小伙伴的沟通,能够经过 new Image() 调接口,确实是个缝隙

// 不需求创立DOM 发送图片恳求
let img = new Image();
img.src= "";

黑名单中增加’Image’字段,堵上这个缝隙。假如还有其他缝隙,欢迎沟通评论

总结

经过解决面试官提出的问题,介绍了沙箱的基本概念、使用场景,以及怎么去完成符合要求的沙箱,发现避免沙箱逃逸是一件挺风趣的工作,就像双方在下棋相同,你来我往,有攻有守

关于这个问题,小伙伴们假如有其他可行的计划,或许有要弥补、指正的,欢迎沟通评论

阿里面试官:请规划一个不能操作DOM和调接口的环境

参考资料:
浅析 JavaScript 沙箱机制