AI 年度盘点与2025发展趋势展望,50+案例解析亮相AICon 了解详情
写点什么

如何安全的运行第三方 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:333248

评论

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

搜狐全员遭诈骗,暴露哪些问题?

Authing

身份云 信息 统一身份认证 mfa

怎样让智能电视更加智能

Geek_99967b

小程序 小程序容器 智能电视

IM即时通讯为企业业务移动化带来了怎样的便利?

WorkPlus

多个平台显示IP地址:如何看待互联网实名制

石头IT视角

经典递归 - 青蛙跳台阶问题

芒果酱

递归 C语言 5月月更

Linux环境显式使用动态库

Loken

音视频 5月月更

【高并发】优化加锁方式时竟然死锁了!!

冰河

并发编程 多线程 高并发 协程 异步编程

JavaScript获取元素的宽高

空城机

JavaScript 5月月更

LabVIEW操作鼠标滚轮放大/缩小图像

不脱发的程序猿

图像处理 LabVIEW 鼠标滚轮来放大和缩小图片

Seektiger DAO的共识增长计划,锁仓STI长线价值投资

股市老人

JVM进阶(十九)——Class文件常量池

No Silver Bullet

JVM class 5月月更

面试突击51:为什么单例一定要加 volatile?

王磊

Java 面试

什么是小程序运行时框架?

Geek_99967b

native 小程序容器 小程序开发

刷新三项世界纪录!浪潮云ICP ARM架构获SPEC Cloud测试全球第一名

云计算

LabVIEW实现Modbus通信

不脱发的程序猿

LabVIEW Modbus 串口通信 VISA 工业通信

LabVIEW虚拟数字示波器

不脱发的程序猿

数据采集 LabVIEW 虚拟示波器 信号发生VI

DNS解析过程

工程师日月

5月月更

Netty实战-实现内网穿透(一)

风斩断晚霞

Java Go Netty 内网穿透

Docker下RabbitMQ四部曲之一:极速体验(单机和集群)

程序员欣宸

Java RabbitMQ 5月月更

云原生数据库的下一次变革发生在哪里?

亚马逊云科技 (Amazon Web Services)

数据库 云原生

DataPipeline完成B+轮1.2亿元人民币融资,定义基于DataOps理念的下一代数据基础设施

DataPipeline数见科技

Flutter的特别之处

Geek_99967b

flutter 小程序开发

Kubernetes Gateway API - 服务网络的演进

Flomesh

Kubernetes Gateway ingress

云端的 ABAP Restful 服务开发以及通过 abapGit 传输到其他系统的详细步骤

汪子熙

git SAP abap 5月月更 abapgit

打码打码Python爬虫,某省建筑市场请求地址参数分析,手慢无爬虫

梦想橡皮擦

5月月更

Go Web 编程入门:中间件

宇宙之一粟

中间件 Go web 5月月更

开源生态|打造活力开源社区,共建开源新生态!

Orillusion

开源 WebGL 元宇宙 Metaverse webgpu

企业上云,安全合规如何进阶 ——一文拆解亚马逊云科技云安全理念与实践

亚马逊云科技 (Amazon Web Services)

云安全 亚马逊云

LabVIEW使用移位寄存器计算平均值

不脱发的程序猿

LabVIEW 移位寄存器计算平均值 移位寄存器

Java Core「5」自定义注解编程

Samson

学习笔记 5月月更 Java core

全栈、云原生的数据分析时代已来,我们如何抓住机会?

亚马逊云科技 (Amazon Web Services)

数据分析 云原生 全栈

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