写点什么

如何安全的运行第三方 JavaScript 代码(下)?

  • 2019-09-30
  • 本文字数:5102 字

    阅读完需:约 17 分钟

如何安全的运行第三方JavaScript代码(下)?

在本文中,我们将为读者详细介绍如何在自己的软件中安全地运行第三方 JavaScript 代码。


使用 Realms 安全地实现 API

总的来说,我们觉得 Realms 的沙箱功能还是非常不错。尽管与 JavaScript 解释器方法相比,我们要处理更多细节,但它仍然可以作为白名单而不是黑名单来运作,这使其实现代码更加紧凑,因此也更便于审计。作为另一个加分项,它还是由受人尊敬的 Web 社区成员所创建的。


但是,单靠 Realms 仍然无法满足我们的要求,因为它只是一个沙箱,插件在其中不能做任何事情。我们仍然需要实现可以供插件使用的 API。这些 API 也必须是安全的,因为大多数插件都需要能够显示一些 UI,以及发送网络请求。


例如,假设沙箱默认情况下不包含 console 对象。毕竟,console 是一个浏览器 API,而不是 JavaScript 功能。为此,我们可以将其作为全局变量传递给沙箱。


realm.evaluate(USER_CODE, { log: console.log })
复制代码


或者将原来的值隐藏在函数中,使沙箱无法修改它们:


realm.evaluate(USER_CODE, { log: (...args) => { console.log(...args) } })
复制代码


不幸的是,这是一个安全漏洞。即使在第二个示例中,匿名函数也是在 realm 之外创建的,却直接提供给了 realm。这意味着插件可以通过 log 函数的原型链逃逸到沙箱之外。


实现 console.log 的正确方法是将其封装到在 realm 内部创建的函数中。这里是一个简化版本的示例(实际上,它还需要对 realms 间抛出异常进行相应的转换处理)。


// Create a factory function in the target realm.// The factory return a new function holding a closure.const safeLogFactory = realm.evaluate(        (function safeLogFactory(unsafeLog) {                return function safeLog(...args) {                        unsafeLog(...args);                }        })); // Create a safe functionconst safeLog = safeLogFactory(console.log); // Test it, abort if unsafeconst outerIntrinsics = safeLog instanceof Function;const innerIntrinsics = realm.evaluate(log instanceof Function, { log: safeLog });if (outerIntrinsics || !innerIntrinsics) throw new TypeError(); // Use itrealm.evaluate(log("Hello outside world!"), { log: safeLog });
复制代码


一般来说,不允许沙箱直接访问在沙箱之外创建的对象,因为这些对象可以访问全局作用域。同样重要的是,应用编程接口在操作沙箱内部的对象时要格外小心,因为这可能跟沙箱外部的对象相混淆。


这就带来一个问题——虽然该方法能用于构建一个安全的应用程序接口,但是开发人员每次向应用程序接口添加一个新函数时,都需要考察对象源在语义上是否有问题。那我们该怎么解决呢?

用于解释器的 API

问题是直接利用 Realms 构建 Figma 应用编程接口的话,则需要对每个 API 端点都进行安全审计,包括其输入和输出值。很明显,这样的话,工作量实在太大了。



尽管 Realms 沙箱中的代码是使用相同的 JavaScript 引擎运行的,但如果假设我们仍然面临 WebAssembly 方法所带来的限制的话,这对我们是非常有帮助的。


回顾一下 Duktape,在尝试#2 章节中,JavaScript 解释器将被编译为 WebAssembly。因此,主线程中的 JavaScript 代码无法直接保存对沙箱内对象的引用。毕竟,在沙箱中,WebAssembly 是通过自己来管理堆的,因此,所有 JavaScript 对象都位于这个堆所在的内存空间中。事实上,Duktape 甚至可能没有使用与浏览器引擎相同的内存表示来实现 JavaScript 对象!


因此,Duktape 的 API 只能借助于低级操作实现,例如一会儿将整数和字符串复制到虚拟机中,一会儿再复制回来。即便可以在解释器中保存对象或函数的引用,但也仅能作为不透明句柄使用。


这种接口看起来像下面这样:


// vm == virtual machine == interpreterexport interface LowLevelJavascriptVm {  typeof(handle: VmHandle): string   getNumber(handle: VmHandle): number  getString(handle: VmHandle): string   newNumber(value: number): VmHandle  newString(value: string): VmHandle  newObject(prototype?: VmHandle): VmHandle  newFunction(name: string, value: (this: VmHandle, ...args: VmHandle[]) => VmHandle): VmHandle   // For accessing properties of objects  getProp(handle: VmHandle, key: string | VmHandle): VmHandle  setProp(handle: VmHandle, key: string | VmHandle, value: VmHandle): void  defineProp(handle: VmHandle, key: string | VmHandle, descriptor: VmPropertyDescriptor): void   callFunction(func: VmHandle, thisVal: VmHandle, ...args: VmHandle[]): VmCallResult  evalCode(code: string): VmCallResult} export interface VmPropertyDescriptor {  configurable?: boolean  enumerable?: boolean  get?: (this: VmHandle) => VmHandle  set?: (this: VmHandle, value: VmHandle) => void}
复制代码


请注意,这些就是 API 实现将要使用的接口,但它或多或少地以一对一的形式映射到 Duktape 的解释器 API。毕竟,Duktape(和类似的虚拟机)的构建正是为了以嵌入形式使用,并允许嵌入方与 Duktape 进行通信。


使用该接口,可以将对象{x: 10, y: 10}传递到沙箱,具体如下所示:


let vm: LowLevelJavascriptVm = createVm()let jsVector = { x: 10, y: 10 }let vmVector = vm.createObject()vm.setProp(vmVector, "x", vm.newNumber(jsVector.x))vm.setProp(vmVector, "y", vm.newNumber(jsVector.y))
复制代码


下面给出用于 Figma 节点对象的“opacity”属性的 API:


vm.defineProp(vmNodePrototype, 'opacity', {  enumerable: true,  get: function(this: VmHandle) {    return vm.newNumber(getNode(vm, this).opacity)  },  set: function(this: VmHandle, val: VmHandle) {    getNode(vm, this).opacity = vm.getNumber(val)    return vm.undefined  }})
复制代码


这个底层接口可以通过 Realms 沙箱很好地实现。这样的实现只需要相对较少的代码(就本例来说,大约为 500 LOC)。不过,我们需要对这一小部分代码进行仔细审计。但是,一旦完成了上述工作,就可以直接利用这些接口来开发其他的 API,而不用担心沙箱方面的安全问题。



从本质上讲,这就是将 JavaScript 解释器和 Realms 沙箱视为“运行 JavaScript 代码的一些独立环境”。


在沙箱上创建低级抽象还需要关注另一个关键问题。虽然我们对 Realms 的安全性充满了信心,但根据经验,在安全方面再小心也不为过。所以,我们不妨假设 Realms 中存在未知的安全漏洞,总有一天会变成我们必须处理的问题。


这就是前面花了许多章节来介绍如何编译一个甚至不用的解释器的原因。因为该 API 是通过一个其实现可以互换的接口实现的,所以,解释器仍然是一个有效的备份计划,我们可以在无需重新实现任何 API 或破坏任何现有插件的情况下启用它。

插件功能的多样性

现在,我们获得了可以安全运行任意插件的沙箱,以及允许这些插件操作 Figma 文档的 API。这就相当于为我们的世界打开了一扇大门。


但是,我们试图解决的最初问题是为设计工具构建插件系统。为提高可用性,这些插件中的大部分都需要具备创建用户界面的功能,并且许多插件还需要具有某种形式的网络访问能力。更一般地说,我们希望插件能够尽可能多地利用浏览器和 JavaScript 的生态系统。


我们可以一次一个地、小心谨慎地公开安全的、受限制的浏览器 API 版本,就像上面的 console.log 示例一样。然而,浏览器 API(尤其是 DOM)的涉及面太大,甚至比 JavaScript 本身还要大。这种尝试可能因限制太多而无法使用,或者可能存在安全缺陷。


我们通过重新引入源为 null 的来解决这个问题。这样的话,插件就可以创建一个并在其中放置任意 HTML 和 Javascript 代码了。



这跟我们最初尝试使用的的区别在于,现在,插件是由两个组件组成:


· 一个可以访问 Figma 文档并在 Realms 沙箱内的主线程上运行的组件。


· 一个可以访问浏览器 API 并在内部运行的组件。


这两个组件可以通过消息传递进行通信。虽然这种架构使得使用浏览器 API 比在同一环境中运行这两个组件要繁琐一些,但是,鉴于目前的浏览器技术的状况,这是安全地运行他人 Javascript 代码的最佳技术,当然,随着技术的进步,将来一定会出现更好的插件创建技术。

小结

经过一段曲折的探索之旅后,我们终于找到了一个实现插件的行之有效的解决方案。借助于 Realm 的 shim 库,我们不仅实现了第三方代码的隔离,同时仍然允许它在开发人员熟悉的类浏览器环境中运行。


虽然这对我们来说是最好的解决方案,但对于每个公司或平台而言,它可能并非最终之选。如果您需要隔离第三方代码,并且具有与我们相同的性能和 API 人体工程学方面的要求,那么我们的解决方案还是非常值得借鉴的;否则的话,可能直接通过 iframe 隔离代码就足够了,而且简单的方案总是上上之选。当然,我们的出发点也是冲着简单去的!(本文转自嘶吼


系列文章:


如何安全的运行第三方 JavaScript 代码(上)?


如何安全的运行第三方 JavaScript 代码(中)?


2019-09-30 14:333450

评论

发布
暂无评论
发现更多内容

Improvements of Job Scheduler and Query Execution on Flink OLAP

Apache Flink

大数据 flink 编程 实时计算 OLAP

衡石BI产品预置明道云数据连接器

明道云

Spring 完美导入 IDEA

阿Q说代码

spring IDEA 4月月更

CTF逆向涉及的各种加密算法

网络安全学海

网络安全 信息安全 CTF 渗透测试 漏洞挖掘

恒源云(Gpushare)_没有你想要的镜像?技巧大放送5!

恒源云

镜像仓库 显卡、gpu

北京市东城区赵海东副区长一行莅临博睿数据参观指导

博睿数据

腾讯云CDW-ClickHouse云原生实践

腾讯云大数据

Clickhouse 云数据仓库

Flink 实践教程-进阶(11):SQL 关联:Regular Join

腾讯云大数据

flink 流计算 流计算 Oceanus

龙蜥开源Plugsched:首次实现 Linux kernel 调度器热升级 | 龙蜥技术

OpenAnolis小助手

Linux 内核 龙蜥社区 Plugsched

云效制品仓库 Packages,不限容量免费用

阿里云云效

云计算 maven 阿里云 npm 制品仓库

阿里Maven仓库不限容量,免费用

阿里云云效

云计算 阿里云 npm Maven仓库 制品仓库

Flink 实践教程-入门(10):Python作业的使用

腾讯云大数据

ffmpeg实现web在线转码

lo

4月月更

Web 3.0的未来产业趋势

王强

Web 3.0

两步实现让antd与IDE和睦相处的处理案例

袋鼠云数栈

大数据 开源

一个公式告诉你:如何提升团队的研发效率?

凌晞

技术管理 研发效率

使用FFMPEG自动剪辑视频

十三

什么是元宇宙?为何要关注它?

CECBC

重塑企业数字化能力,端点科技重磅发布Erda2.0

科技热闻

百度信誉保障服务架构全解析

百度Geek说

后端

墨水屏的“硬伤”与福气

脑极体

Hoo虎符研究院|从多个方面了解公链Tezos和它的 Ithaca 2 升级

区块链前沿News

Hoo 虎符交易所 研究院 tezos

RabbitMQ 实现延时队列(订单定时取消为例)

Ayue、

Rabbit MQ 4月月更

Ribbon从入门到源码解析

李子捌

微服务 SpringCloud Ribbon

netty系列之:netty中的核心MessageToMessage编码器

程序那些事

Java Netty 程序那些事 4月月更

基于 HTML+CSS+JS 的石头剪刀布游戏

海拥(haiyong.site)

html 大前端 游戏 4月月更

融合通信常见问题3月刊 | 云信小课堂

网易云信

前端

GaussDB(for Redis)助力《余烬风暴》实力上线,给您沉浸式魔幻体验

华为云数据库小助手

GaussDB GaussDB ( for Redis )

透过「人月神话」,看清软件开发问题

架构精进之路

软件开发 人月神话 4月日更 4月月更

实时数仓建设

五分钟学大数据

实时计算 4月月更

数栈在湖仓一体上的探索与实践

袋鼠云数栈

数据库 大数据 数据湖 湖仓一体

如何安全的运行第三方JavaScript代码(下)?_编程语言_Rudi Chen_InfoQ精选文章