写点什么

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

  • 2019-09-18
  • 本文字数:3817 字

    阅读完需:约 13 分钟

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

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



最近,我们团队完成了 Figma 插件 API 的开发工作,这样第三方开发人员就可以直接在基于浏览器的设计工具中运行代码。这为第三方开发人员带来便利的同时,也给我们带来许多严峻挑战,比如,如何确保插件中运行的代码不会带来安全问题?


让人更头痛的是,我们的软件是建立在非常规的堆栈之上,因此面临许多工具所没有的约束。我们的设计编辑器是建立在WebGLWebAssembly的基础之上的,其中一些用户界面是利用 Typescript&React 来实现的。并且,我们的软件支持多人同时编辑文件。在这个过程中,浏览器技术为我们提供了很大的支持,同时,也带来了许多的限制。


这篇文章将带你了解我们对完美插件解决方案的探索过程。最终,我们的问题可以归结为一点:如何安全、稳定和高效地运行插件?以下是我们面临的重要约束的简要概述:


1、安全性:插件只有在显示启动时才能访问文件。插件应该被限制在当前文件中。插件不能像 figma.com 那样进行调用。插件不能访问对方的数据,除非是自愿提供的。插件不能篡改 Figma UI 及其行为来误导用户(例如网络钓鱼)。


2、稳定性:插件不能降低 Figma 的速度,使其无法使用。插件不能破坏我们产品中的关键不变量,比如让每个人在查看同一个文件时总是看到相同内容的属性。为了查看文件,不需要管理跨设备/用户的插件安装。对 Figma 产品或内部 API 的修改不会破坏现有的插件。


3、易于开发:插件应该易于开发,以支持充满活力的生态系统。我们的大多数用户都是设计师,可能对 JavaScript 经验不多。开发人员应该能够使用现有的调试工具。


4、性能:插件应该运行得足够快,以支持大多数常见的场景,例如搜索文档、生成图表等等。


尝试 #1:沙箱方法


在我们最初几周的研究工作中,我们尝试了多种第三方代码沙箱,其中一些使用了诸如代码到代码间转换的技术。然而,大多数沙箱都没有在应用程序产品中经过长时间的历练,因此,使用这些沙箱肯定存在一定的风险。


最后,作为我们的第一次尝试,我们使用了最接近标准沙箱解决方案的一种方法:标签。该方法适用于需要运行第三方代码的应用程序,如 CodePen。


需要注意的是,这里的并不是我们平常使用的 HTML 标签。要理解方法为什么能够提供安全性,就必须先来了解一下它提供了哪些特性。


一般来说,通常用于将一个网站嵌入到另一个网站中。例如,在下图中,你可以看到Yelp.com网站中嵌入了Google.com/Maps,这样就可以为用户提供地图功能。



在这里,我们当然不希望因 Yelp 嵌入谷歌地图功能就能读取 Google 网站的内容,因为那里可能存有用户的私人信息。同样,你也不希望谷歌因此而获得了访问 Yelp 网站的内容权限。


这意味着之间的通信应该受到浏览器的严格限制。当的来源与其容器(如yelp.com与google.com)不同时,它们应该是完全隔离的。同时,与进行通信的唯一方法是通过消息传递。实际上,这些消息就是一些纯字符串。收到消息后,每个网站都可以对这些消息采取相应的行动,也可以对它们置之不理。


事实上,它们是如此独立,以至于 HTML 规范允许浏览器将列为单独进程,只要他们喜欢的话。


既然了解了的工作原理,我们就可以通过在每次插件运行时创建一个新的,并将插件的代码粘贴在中来实现插件,这样,插件可以在中做任何想做的事情。但是,除非消息通过了显示的白名单检测,否则,它无法与 Figma 文档进行交互。也是一种特殊的 null 源,这意味着向 figma.com 发送请求的尝试都会被浏览器的跨源资源共享策略所拒绝。



实际上,在这里充当了插件的沙箱角色,而浏览器供应商则为我们提供了沙箱的安全保证,毕竟他们多年来一直在忙着搜索和修复沙箱中的各种漏洞。


使用这个沙箱模型的实际插件将使用我们添加到沙箱中的一个应用程序接口,具体如下所示:


const scene = await figma.loadScene() // gets data from the main threadscene.selection[0].width *= 2scene.createNode({  type: 'RECTANGLE',  x: 10, y: 20,  ...})await figma.updateScene() // flush changes back, to the main thread
复制代码


这里的重点在于,插件是通过调用 loadScene(它向 Figma 发送消息以获取文档的副本)来进行初始化,并通过调用 updateScene(将插件所修改的发送回 Figma)作为其结束的。请注意:


  • 我们是通过获取文档的副本,而不是使用消息传递来完成属性的读取和写入操作。传递消息时,每次往返需耗时 0.1ms,这样的话,每秒只能传递 1000 条左右的消息。

  • 我们不会让插件直接使用 postMessage,因为这样做很麻烦。


决定采用这种方法后,我们大约用了一个月的时间构建好相应的 API。当时来看,马上就大功告成了,我们甚至邀请了一些 alpha 测试人员。然而,我们很快就发现,这种方法存在两大缺陷。


问题 1:async/await 关键字对用户来说不够友好


我们得到的第一反馈是,人们讨厌使用 async/await 关键字——但是在这种方法中,这是不可避免的。消息传递本质上就是异步操作,而在 JavaScript 中是没有办法对异步操作进行同步阻塞式的调用。


对于这种方法,我们不仅需要使用 await 关键字,同时还需要将所有调用函数标签为 async。综上所述,异步/等待仍然是一个比较新颖的 JavaScript 功能,要想玩转它,需要对并发性概念有相当深入的理解——很明显,这对于我们的插件开发人员来说,要求太高。


不过,如果只需要在插件开头和结尾处各使用一次 await 关键字的话,情况就没有那么糟糕。我们只需要告知开发人员始终将 await、loadScene 和 updateScene 搭配使用即可,即使他们不太了解它们的作用,影响也不大。


问题是某些 API 调用需要运行许多复杂的逻辑。例如,有时更改某图层上的单个属性后,必须同时更新其他多个图层。例如,调整 frame 的大小后,需要递归地将约束应用于其子 frame。


这些行为通常涉及许多行为复杂且差别细微的算法。如果因插件而重新实现这些算法的话,肯定不是一个好主意。此外,这些逻辑还会被编译到 WebAssembly 二进制文件中,因此,重用起来并不容易。如果我们不在插件沙箱中运行这些逻辑的话,插件将会读取过时的数据。


所以,尽管这种方法具有一定的可行性,但还是比较麻烦。例如:


await figma.loadScene()... do stuff ...await figma.updateScene()
复制代码


即使是经验丰富的工程师,事情也很快变得非常棘手:


await figma.loadScene()... do stuff ...await figma.updateScene()await figma.loadScene()... do stuff ...await figma.updateScene()await figma.loadScene()... do stuff ...await figma.updateScene()
复制代码


问题 2:场景的复制成本很贵


方法的第二个问题是,在将文档的大部分内容发送到插件之前,需要先对其进行序列化。


事实证明,人们有时会在 Figma 软件中创建非常非常大的文档,甚至达到内存的上限。例如,对于微软的设计系统文件(去年我们花了一个月时间对其进行优化)来说,将文档序列化并将其发送给插件就需要花费 14 秒时间——这些还是发生在插件运行之前。鉴于大多数插件都涉及快速的操作,例如“交换选中的两个对象”,这将使插件的可用性作废。


以增量方式加载数据或延后加载数据也不是一个好的选择,因为:


1.要想重构核心产品,至少需要花上几个月的时间。


2.所有需要等待尚未到达的数据的 API,都是异步的。


总之,因为 Figma 文档可能包含大量相互依赖的数据,所以对我们来说是不可取的。


主线程方法


由于排除了方法,我们不得不另觅他途。此后的两个周,我们又尝试了许多方法,但是或多或少存在某些不可接受的缺陷:


  • API 难以使用(例如,需要使用 REST API 或类似 GraphQL 的方法访问文档)

  • 需要借助浏览器供应商已放弃或正打算放弃的浏览器功能(例如,同步 xhr 请求+Service Worker、共享缓冲区)

  • 需要大量的研究工作或重新构建我们的应用程序,这可能需要花费几个月的时间来验证其可行与否(例如,通过 CRDT,利用 iframe+sync 方式加载 Figma 的副本等)


最终,我们终于得出结论:必须找到一种方法来创建一个模型,其中插件可以直接操作文档。这样,编写插件在一定程度上就是实现手动操作的自动化,为此,我们必须允许插件在主线程上运行。(本文转自嘶吼)


(未完待续)


原文链接:


https://www.4hou.com/technology/20153.html


2019-09-18 16:462705

评论

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

LeetCode题解:169. 多数元素,分治,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

性能测试界“网红”云性能测试服务,了解一下?

华为云开发者联盟

CloudTest 沙箱实验 云性能测试

《迅雷链精品课》第七课:以太坊数据存储分析

迅雷链

区块链

都是“算法”惹的祸,字节三面处处坑,我的offer要凉了?

Java~~~

字节跳动 面试 编程语言 算法和数据结构

云原生应用Go语言:你还在考虑的时候,别人已经应用实践

华为云开发者联盟

微服务 云技术 Go 语言

基于DAYU的实时作业开发,分分钟搭建企业个性化推荐平台

华为云开发者联盟

华为 算法 数据 dayu

什么是堡垒机?为什么需要堡垒机?

xcbeyond

运维

三分钟带你搞懂分布式链路追踪系统原理

Java架构师迁哥

Alibaba最新《Java架构核心宝典》限时开放下载,互联网主流技术详解总结,提升技术能力的必备宝典!

Java架构之路

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

申通快递 双11 云原生应用实践

阿里巴巴云原生

阿里云 Kubernetes 运维 云原生 监控

有奖话题 | 如果程序员和产品经理都会凡尔赛文学,将如何对话?

YourBatman

话题讨论 凡尔赛文学

原创 | 使用JPA实现DDD持久化-只要O,忘记R & Maven配置

编程道与术

Java hibernate 编程 mybatis jpa

视频作品播放量低:自媒体作者如何走出新手村

石头IT视角

原创 | TDD工具集:JUnit、AssertJ和Mockito (二十七)运行测试-在构建工具中运行测试

编程道与术

Java 编程 TDD 单元测试 JUnit

英特尔与南京溧水经济技术开发区共同成立智能交通研究院

E科讯

程序员面试的时候突然遇到答不上的问题怎么办?

Java架构师迁哥

接口请求(get、post、head等)详解

测试人生路

HTTP

架构师训练营第 1 期-week10

习习

《华为数据之道》读书笔记:第 3章 差异化的企业数据分类管理框架

方志

数据中台 数据仓库 数据治理 元数据

马士兵最新2020涵盖P5—P8Java全栈架构师学习路线,跟着老师学我已拿P7Offer!

Java架构追梦

Java 学习 架构 面试 马士兵

如何用CSS实现图像替换链接文本显示并保证链接可点击

陈北

CSS小技巧

纷享销客罗旭:拐点下的中国SaaS

ToB行业头条

SaaS

架构师训练营 1 期 -- 第十周总结

曾彪彪

极客大学架构师训练营

理解三值逻辑与NULL,你离SQL高手更近了一步

华为云开发者联盟

sql null 逻辑

大厂都是怎么用Java8代替SimpleDateFormat?

Java架构师迁哥

原创 | 使用JPA实现DDD持久化-数据库连接配置:persistence.xml

编程道与术

Java hibernate 编程 mybatis jpa

我是面试官,我来分享一波面经!看看我的内心OS

比伯

Java 编程 架构 面试 技术宅

双指针算法和位运算&离散化和区间合并

落曦

容器化时代到来!跳转机分配问题终于“有救”了

华为云开发者联盟

容器 镜像 网络

打工人、打工魂、高效MES助力打工者都是人上人

Marilyn

敏捷开发 快速开发 MES系统

为什么程序员不做外包

Java架构师迁哥

如何安全的运行第三方JavaScript代码(上)?_安全_Rudi Chen_InfoQ精选文章