写点什么

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

评论

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

堪称神作!阿里数位专家联合写的“大厂高频Java面试手册”

码农之家

Java 编程 程序员 互联网 面试

阿里高工熬夜14天码出这份Java10w字的面试手册!却遭GitHub封杀

Java架构之路

Java 程序员 架构 面试 编程语言

阿里P8整理出SQL笔记:收获不止SOL优化抓住SQL的本质

Java架构之路

Java 程序员 架构 面试 编程语言

CopyOnWriteArrayList源码解读之CopyOnWrite思想的利与弊

徐同学呀

Java源码 JUC CopyOnWriteArrayList

聪明人的训练(十七)

Changing Lin

4月日更

边缘计算是流行词还是风口?开发者怎样选开源项目?

华为云开发者联盟

开源 开发者 5G 边缘计算 EdgeGallery 社区

2021互联网大厂高频面试专题500道:并发编程/Spring/MyBatis(附答案解析)

比伯

Java 编程 架构 程序人生 计算机

架构师实战营 模块二作业(微信朋友圈高性能复杂度架构分析)

代廉洁

架构实战营

阿里高工熬夜18天码出Java150K字面试宝典,却遭Github全面封杀

Java架构之路

Java 程序员 架构 面试 编程语言

为极客时间增加自动提醒功能,督促用户回来上课

克比

史上最强的:京东北极星商业系统权限管控实践

Java架构师迁哥

计算机原理学习笔记 Day8

穿过生命散发芬芳

计算机原理 4月日更

MySQL 索引概要

大海

MySQL 索引

探索区块链Baas平台的奥秘,源中瑞公共服务平台开发技术

源中瑞-龙先生

区块链 源中瑞 Baas

程序员3年CRUD从8K涨到20K,这4个月我到底经历了什么?

码农之家

编程 程序员 互联网 面试 职场

MySQL存储过程的异常处理

Sakura

4月日更

阿里P8重磅总结:看完别说不会了哦,SpringBoot「完结篇」

比伯

Java 编程 程序人生 计算机 架构】

还有人搞不懂数据仓库与数据库的区别?

大数据技术指南

数据仓库 4月日更

架构师实战营 模块二总结

代廉洁

架构实战营

FutureTask源码解读,阻塞获取异步计算结果(阻塞、取消、装饰器、适配器、Callable)

徐同学呀

Java源码 JUC Future

ThreadPoolExecutor源码解读(一)重新认识ThreadPoolExecutor(核心参数、生命周期、位运算、ThreadFactory、拒接策略)

徐同学呀

线程池 Java源码 JUC ThreadPoolExecutor

技术实践丨列存表并发更新时的锁等待问题原理

华为云开发者联盟

事务 update 元组 列存表

第 0 期架构训练营模块 2 作业

架构实战营

Anolis OS 8.2 RC2 发行,支持飞腾、海光、兆芯、鲲鹏等芯片

阿里云基础软件团队

架构实战营 模块二作业

fazinter

架构实战营

Impala架构详解

五分钟学大数据

4月日更 impala

openLooKeng如何应对“野蛮零散”的大数据

openLooKeng

大数据 开源 openLooKeng

关于ReentrantReadWriteLock,首个获取读锁的线程单独记录问题讨论(firstReader和firstReaderHoldCount)

徐同学呀

AQS Java源码 JUC

这才是大数据的正确打开方式

华为云开发者联盟

大数据 数据仓库 云原生 数据治理 灾备

Spring Bean创建过程的Hook

邱学喆

BeanPostProcessor @Autowired注入原理 @Resource注入原理 @Value注入原理

Github霸榜数月!原来是阿里大牛最新的Java性能优化实战笔记

钟奕礼

Java 编程 程序员 架构 面试

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