原创 码中仙
一、什么是沙箱环境
在计算机安全中,沙箱(Sandbox)是一种用于隔离正在运行程序的安全机制,通常用于执行未经测试或不受信任的程序或代码,它会为待执行的程序创建一个独立的执行环境,内部程序的执行不会影响到外部程序的运行。
其实在前端世界里,沙箱环境无处不在!
例如以下几个场景:
1、Chrome本身就是一个沙箱环境
Chrome 浏览器中的每一个标签页都是一个沙箱(sandbox)。渲染进程被沙箱(Sandbox)隔离,网页 web 代码内容必须通过 IPC 通道才能与浏览器内核进程通信,通信过程会进行安全的检查。
2、在线代码编辑器(码上掘金、CodeSandbox、CodePen等)
在线代码编辑器在执行脚本时都会将程序放置在一个沙箱中,防止程序访问/影响主页面。
3、Vue的 服务端渲染
在 Node.js 中有一个模块叫做 VM,它提供了几个 API,允许代码在 V8 虚拟机上下文中运行。
const vm = require('vm');
const sandbox = { a: 1, b: 2 };
const script = new vm.Script('a + b');
const context = new vm.createContext(sandbox);
script.runInContext(context);
vue的服务端渲染实现中,通过创建沙箱执行前端的bundle文件;在调用createBundleRenderer方法时候,允许配置runInNewContext为true或false的形式,判断是否传入一个新创建的sandbox对象以供vm使用。
4、Figma 插件
出于安全和性能等方面的考虑,Figma将插件代码分成两个部分:main 和 ui。其中 main 代码运行在沙箱之中,ui 部分代码运行在 iframe 之中,两者通过 postMessage 通信。
5、微前端
典型代表是 Garfish和qiankun
从0开始实现一个JS沙箱环境
1、 最简陋的沙箱(eval)
问题:
要求源程序在获取任意变量时都要加上执行上下文对象的前缀
eval的性能问题
源程序可以访问闭包作用域变量
源程序可以访问全局变量
2. eval + with
问题:
eval的性能问题
源程序可以访问闭包作用域变量
源程序可以访问全局变量
3、 new Function + with
问题:
源程序可以访问全局变量
4. ES6 Proxy
我们先看Proxy的使用
Proxy
给 {}
设置了属性访问拦截器,倘若访问的属性为 a 则返回 1,否则走正常程序。
Proxy 支持的拦截操作,一共 13 种:
get(target, propKey, receiver) :拦截对象属性的读取,比如proxy.foo和proxy['foo']。
set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
has(target, propKey) :拦截propKey in proxy的操作,返回一个布尔值。
deleteProperty(target, propKey) :拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target) :拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target, propKey) :拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc) :拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target) :拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target) :拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target) :拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto) :拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
在沙箱环境中,对本身不存在的变量会追溯到全局变量上访问,此时我们可以使用 Proxy "欺骗" 程序,告诉它这个「不存在的变量」是存在的。
报错了,因为我们阻止了所有全局变量的访问。
继续改造:
Symbol.unscopables:
Symbol
是 JS 的第七种数据类型,它能够产生一个唯一的值,同时也具备一些内建属性,这些属性可以用来进行元编程(meta programming),即对语言本身编程,影响语言行为。其中一个内建属性 Symbol.unscopables
,通过它可以影响 with
的行为,从而造成沙箱逃逸。
对这种情况做一层加固,防止沙箱逃逸
到这一步,其实很多较为简单的场景就可以覆盖了(比如: Vue 的模板字符串)。
仍然有很多漏洞:
code 中可以提前关闭 sandbox 的 with 语境,如 '} alert(this); {'
code 中可以使用 eval 和 new Function 直接逃逸
code 中可以通过访问原型链实现逃逸
更为复杂的场景,如何实现任意使用诸如 document、location 等全局变量且不会影响主页面。
5. iframe是天然的优质沙箱
iframe 标签可以创造一个独立的浏览器原生级别的运行环境,这个环境由浏览器实现了与主环境的隔离。在 iframe 中运行的脚本程序访问到的全局对象均是当前 iframe 执行上下文提供的,不会影响其父页面的主体功能,因此使用 iframe 来实现一个沙箱是目前最方便、简单、安全的方法。
如果只考虑浏览器环境,可以用 With + Proxy + iframe 构建出一个比较好的沙箱:
利用 iframe 对全局对象的天然隔离性,将 iframe.contentWindow 取出作为当前沙箱执行的全局对象
将上述沙箱全局对象作为 with 的参数限制内部执行程序的访问,同时使用 Proxy 监听程序内部的访问。
维护一个共享状态列表,列出需要与外部共享的全局状态,在 Proxy 内部实现访问控制。
6. 基于ShadowRealm 提案的实现
ShadowRealm API 是一个新的 JavaScript 提案,它允许一个 JS 运行时创建多个高度隔离的 JS 运行环境(realm),每个 realm 具有独立的全局对象和内建对象。
这项特性提案时间为 2021 年 12 月,目前在Stage 3阶段 tc39.es/proposal-sh…[1]
evaluate(sourceText: string) 同步执行代码字符串,类似 eval()
importValue(specifier: string, bindingName: string) 异步执行代码字符串
7. Web Workers
Web Workers代码运行在独立的进程中,通信是异步的,无法获取当前程序一些属性或共享状态,且有一点无法不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现。
以上就是实现JS沙箱隔离的一些思考点。在真实的业务应用中,没有最完美的方案,只有最合适的方案,还需要结合自身业务的特性做适合自己的选型。
本文转自 (https://juejin.cn/post/7410347763898597388),如有侵权,请联系删除。
参考资料
[1]https://tc39.es/proposal-shadowrealm/: https://link.juejin.cn/?target=https%3A%2F%2Ftc39.es%2Fproposal-shadowrealm%2F