前言
四面的时分被问到了这个问题,其时第一时刻没有反应过来,觉得这个需求好独特
面试官给了一些提示,我才理解这道标题的意思,终究答复的也是磕磕绊绊
后来花了一些时刻整理了下思路,那么怎么规划这样的环境呢?
终究完成
完成思路:
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();
};
浏览器直接报错
在 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!
履行成果:
天然的优质沙箱(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);
打印成果:
持续优化
经过与评论区小伙伴的沟通,能够经过 new Image()
调接口,确实是个缝隙
// 不需求创立DOM 发送图片恳求
let img = new Image();
img.src= "";
黑名单中增加’Image’字段,堵上这个缝隙。假如还有其他缝隙,欢迎沟通评论
总结
经过解决面试官提出的问题,介绍了沙箱的基本概念、使用场景,以及怎么去完成符合要求的沙箱,发现避免沙箱逃逸是一件挺风趣的工作,就像双方在下棋相同,你来我往,有攻有守
关于这个问题,小伙伴们假如有其他可行的计划,或许有要弥补、指正的,欢迎沟通评论
参考资料:
浅析 JavaScript 沙箱机制