脚本安全(Scripting Security)
Rive 文件可以包含代码:AnimaScript 与 Luau 脚本,以及 WGSL 着色器。Rive 运行时把这些代码当作不可信内容。它在沙箱中运行,只能做 Rive 允许的事,因此无法访问网络、设备上的文件,或应用的其余部分。
AnimaScript 是新脚本的推荐语言。Luau 已弃用:Luau 脚本仍会在每个 Rive 运行时上继续运行,但新的脚本功能会落到 AnimaScript。本页两者都覆盖。
常见问题(FAQs)
| 问题 | 回答 |
|---|---|
| 脚本能把数据送出应用吗? | 不能。 Rive 运行时不给脚本访问网络、文件或其他应用的能力。脚本只能通过视图模型(View Models)和事件,把数据交给你自己的代码。参见文件中的代码能做什么。 |
| 文件能让运行时发出网络请求吗? | 仅用于下载 Hosted 资源,且仅在默认会这样做的运行时上。脚本不能向这些请求附加数据,你也可以关闭下载。参见 Hosted 资源。 |
| 脚本能读到什么? | 你的应用绑定到该文件的视图模型数据、到达 Rive 视图的输入,以及当前时间。参见处理敏感数据的应用建议。 |
| 文件能打开 URL 吗? | 默认不能。 Apple 和 Android 运行时不会打开来自文件的 URL;Web 运行时只有在你开启后才会。参见 Open URL 事件。 |
| 文件能导航应用吗? | 不能。 你的代码决定如何处理事件和视图模型值。参见导航。 |
| 文件中的代码有签名吗? | Luau 脚本和着色器有签名,没有有效签名时运行时会拒绝它们。AnimaScript 不需要签名,因为 WebAssembly 沙箱会检查并约束每个模块。参见签名保证什么。 |
| 签名能证明什么? | 代码由 Rive 的编译器生成,且之后无人改动。它不能证明代码是你的。任何人都可以从自己的 Rive 账户导出带签名的代码。参见签名不能证明什么。 |
| 签名覆盖整个文件吗? | 目前不是。 它只覆盖已编译的 Luau 脚本和着色器。整文件签名即将推出。 |
| 运行时能否只接受我们的文件? | 目前不能。 在加载前于应用内检查每个文件。参见从设计到发布之间保护文件和整文件签名。 |
| 能否关闭脚本? | 可以,使用不含脚本的运行时构建。没有针对单个文件的开关。参见关闭脚本。 |
Rive 如何看待脚本安全(How Rive approaches script security)
- 沙箱就是安全边界。 Rive 不依赖信任文件的作者。文件中的代码在限制其可达范围的沙箱里运行,因此即使是有害脚本也无法触达 Rive 不允许的任何东西。
- 脚本一开始什么都没有。 Rive 有意、以它控制的 API 逐项加入能力:绘制、视图模型、输入、音频,以及脚本所属文件中的对象。脚本不能使用 Rive 没有给予的任何东西。
- 签名证明代码从哪来,不证明它安全。 Rive 只在引擎无法自行检查代码的地方要求签名:Luau 字节码和已编译着色器。Rive 在自己的服务器上编译这些代码,因此只签署它自己生成的代码。AnimaScript 不需要签名,因为 WebAssembly 沙箱会检查并约束每个模块。
- 违反规则的脚本会以错误停止。 你的应用继续运行。时间限制能拦住大多数运行过久的脚本调用,但不是全部;脚本仍可能占用足够的 CPU、GPU 或内存,让应用变慢或卡住。各限制覆盖范围见时间限制。
- 由你的应用做决定。 文件不能直接对你的应用采取行动。它只能把值交给你的代码,由你的代码决定如何处理。
文件中的代码能做什么(What code in a file can do)
Rive 文件中的一切(包括代码)都在你的应用给它的视图内工作。AnimaScript 与 Luau 脚本受到相同的限制。
文件可以:
- 在其视图内绘制。它不能画到视图外,也不能改动应用其余 UI。
- 播放文件中存储的声音。AnimaScript 脚本还可以生成声音。
- 对到达其视图的输入作出反应:指针、键盘、手柄和文本输入。
- 读取并修改你的应用绑定给它的视图模型数据。
- 向你的应用报告事件。
脚本为这些能力加入逻辑。脚本还可以读取当前时间、向调试控制台打印消息,并用 GPU Canvas 着色器 绘制。AnimaScript 脚本还可以运行 job 和 worker;在运行时支持的地方,它们跑在后台线程上。
文件及其脚本都不能:
- 发起网络请求。唯一例外是下载 Hosted 资源,由运行时完成,并由你的应用控制。参见 Hosted 资源。
- 读写设备上的文件。
- 读取设备数据,例如位置、通讯录、照片、相机、麦克风或剪贴板。
- 读取你尚未绑定到该文件的应用数据。
- 触达应用已加载的其他 Rive 文件。每个文件在自己的环境中运行脚本。
- 从文件外部加载代码,或在运行时创建新代码。
- 导航你的应用。
- 打开 URL,除非你开启。只有 Web 运行时可以这样做,使用已弃用的
automaticallyHandleEvents选项。参见 Open URL 事件。
各语言如何被约束(How each language is contained)
| AnimaScript | Luau | |
|---|---|---|
| 状态 | 新脚本的推荐语言 | 已弃用,但现有脚本继续运行 |
| 在文件中的存储形式 | WebAssembly 模块 | Luau 字节码 |
| Web 运行时 | 在浏览器的 WebAssembly 引擎上运行 | 在 Luau 虚拟机上运行,该虚拟机本身编译为 WebAssembly |
| Apple 和 Android 运行时 | 尚不支持,因此文件会加载并播放,但不运行其中的 AnimaScript 脚本 | 在 Luau 虚拟机上运行 |
| 是否签名 | 否 | 是 |
| 约束方式 | WebAssembly 沙箱 | Luau 沙箱,外加签名 |
AnimaScript
AnimaScript 编译为 WebAssembly,这是一种为在沙箱中安全运行代码而设计的格式。Rive 编辑器或 Rive CLI 把你的脚本编译成 WebAssembly 模块,并存储在 .riv 文件中。
- 在 Web 上,运行时把模块加载进浏览器自己的 WebAssembly 引擎,也就是浏览器用于所有 WebAssembly 的同一沙箱。
- 在浏览器之外,Rive 用开源 WebAssembly 运行时 WAMR 运行模块,规则相同。.riv 文件只以 WebAssembly 字节码携带这些模块,从不以机器码形式携带,运行时拒绝其他形式。视平台而定,模块会以提前编译的机器码运行,或在 WAMR 解释器上运行。Apple 和 Android 运行时尚未运行 AnimaScript。参见 Where Scripts Run。
在沙箱内:
- 模块只能调用 Rive 提供的函数。它没有其他途径触达系统。
- 每次内存访问都留在模块自己的内存内。越界访问,或把坏指针传给 Rive API,都会让模块以错误停止。
- 线程和音频处理器在各自的沙箱模块中运行。它们共享的任何内存都属于同一文件的脚本。
AnimaScript 目前没有签名。它的安全性来自沙箱:无论谁构建,沙箱都会检查并约束每个模块。
在 AnimaScript 源码中,原始内存访问必须放在标为 unsafe 的函数里,这样审阅者可以用文本搜索找到它。即便如此,它也只能触达模块自己的内存。参见 The Unsafe Gate。
Luau
Rive 在沙箱化的 Luau 虚拟机中运行 Luau 脚本,并使用精简的标准库:
- 基础函数,不含
loadstring table、math、string、utf8、buffer和bit32os,仅限clock、date、difftime和time
没有 io、package 或 debug 库。内置库和全局是只读的,require 只查找同一文件中的脚本。
Luau 虚拟机假定它运行的字节码来自 Luau 编译器,这也是 Luau 建议对编译后的代码签名 的原因。Rive 运行时只在签名有效时运行 Luau 字节码。参见签名保证什么。
着色器(Shaders)
AnimaScript 和 Luau 脚本都可以用 WGSL 编写的 GPU Canvas 着色器绘制。导出时 Rive 的服务器会编译并签署着色器,没有有效签名的着色器代码会被运行时拒绝。着色器只能使用其脚本提供给它的缓冲区和纹理。
时间限制(Time limits)
- AnimaScript: 每次进入脚本的调用都有时间预算,运行时中为 200 ms。原生运行时用计时器强制执行。在浏览器中,只有模块在构建时带了 fuel 检查,调用才能被打断。调试构建始终包含它们。每次发布生成的优化构建,只有在
wasmFuel: true时才包含。没有 fuel 检查时,永不返回的脚本会卡住页面。用dangerouslyFast构建的文件完全没有时间预算。后台 job 和 worker 没有时间限制;它们会一直运行到完成,或被运行时停止。参见 Build Settings。 - Luau: Rive 运行时会打断运行超过 200 ms 的 Luau 脚本调用。刻意绕过该打断的脚本可以继续运行。
把这些限制当作防呆,而不是防攻击。刻意造成伤害的文件仍可能占用足够的 CPU、GPU 或内存,让应用卡住或崩溃。
签名保证什么(What signing guarantees)
Rive 签署已编译的 Luau 脚本和着色器。当你从 Rive 编辑器导出文件,或用 Rive CLI 发布时,Rive 的服务器会编译源码并签署结果。Rive 自己编译代码,因此只签署它生成的代码。私钥保存在 Rive 的服务器上。每个支持脚本的 Rive 运行时都携带对应公钥,并在加载文件时检查签名。未签名、或签名后又被改动的代码不会运行。
Rive 编辑器和 Rive CLI 会在 Luau 脚本签名之前运行它们,方便你开发。运行时不会。
签名检查是开源运行时的一部分。参见 text_asset_importer.cpp 以及 file.cpp 中的
File::acceptsScript。
签名不能证明什么(What a signature does not prove)
- 谁制作了该文件,或其脚本是你的。 目前,无论账户或工作区,Rive 用同一把密钥签署每一次导出。任何人都可以在自己的 Rive 账户里编写 Luau 脚本,并导出带有效签名的文件,因此换成别人脚本的文件也能通过检查。沙箱限制这些脚本能做什么。签名不会。
- 文件的其余部分未被改动。 目前签名只覆盖已编译的 Luau 脚本和着色器。
签名覆盖什么(What the signature covers)
签名覆盖文件中 Luau 脚本和着色器的已编译代码。它不覆盖文件的其余部分,包括:
- 画板、时间轴和状态机
- 文本、图像、字体和音频
- 默认视图模型值和脚本输入值
- 事件设置,例如 Open URL 事件的地址
- 哪些对象使用哪些脚本
- AnimaScript 模块
如果有人改动这些部分,运行时无法察觉。如果有人在文件内编辑 Luau 脚本或着色器,签名检查会失败,运行时不会运行该文件的任何 Luau 脚本和着色器。文件的其余部分仍会播放。
整文件签名(Whole-file signing)
整文件签名即将推出。计划覆盖整个导出的 .riv 文件,包括 AnimaScript 模块,并且运行时计划拒绝签名检查失败的文件。那时文件中任何一处改动都会阻止加载。我们也在做一种用签名判断哪个工作区导出了该文件的方式。签名届时就能证明文件从哪来。沙箱仍然是安全边界。
从设计到发布之间保护文件(Protecting files between design and release)
目前,任何从 Rive 导出、带有 Luau 脚本或着色器的文件都带有效签名,因此签名无法把你的文件和别人的文件区分开。请像保护应用代码和其他资源一样,保护每个文件从 Rive 编辑器到用户的路径。整文件签名上线后会有帮助。即便如此,下面的检查仍用于确认文件就是你批准的那一份。
打包进应用的文件(Files bundled in your app)
iOS 和 Android 要求覆盖应用包内每个文件的代码签名。发布后,无人能在不破坏该签名的情况下更改打包的 .riv 文件。风险更早出现:文件从设计交到工程时。为降低风险:
- 把已批准的 .riv 文件放进版本控制,并且只通过代码审查改动它们。
- 记录每个已批准文件的 SHA-256 哈希,并在构建流水线中校验。
应用下载的文件(Files your app downloads)
如果应用会下载 .riv 文件,请在加载前检查每一个。
通过 HTTPS 从你控制的服务器提供文件。
把文件的 SHA-256 哈希与内置于应用的值比较,或验证用你自己的密钥做的签名。不要从存放该文件的同一服务器获取期望哈希。
只有检查通过后,才把字节交给 Rive 运行时。
Swift
import CryptoKit
import Foundation
enum RiveFileError: Error { case integrityCheckFailed }
// SHA-256 of the approved file, as lowercase hex, built into the app.
let expectedHash = "<sha-256 of the approved file>"
// fileURL points to the downloaded file on disk.
func verifiedRiveData(from fileURL: URL) throws -> Data {
let data = try Data(contentsOf: fileURL)
let hash = SHA256.hash(data: data)
.map { String(format: "%02x", $0) }
.joined()
guard hash == expectedHash else { throw RiveFileError.integrityCheckFailed }
return data // Pass these bytes to the Rive runtime.
}
Kotlin
import java.io.File
import java.security.MessageDigest
// SHA-256 of the approved file, as lowercase hex, built into the app.
const val EXPECTED_HASH = "<sha-256 of the approved file>"
// file is the downloaded file on disk.
fun verifiedRiveBytes(file: File): ByteArray {
val bytes = file.readBytes()
val hash = MessageDigest.getInstance("SHA-256").digest(bytes)
.joinToString("") { "%02x".format(it) }
check(hash == EXPECTED_HASH) { "Rive file failed its integrity check" }
return bytes // Pass these bytes to the Rive runtime.
}
要得到文件的哈希,在 macOS 上运行 shasum -a 256 your-file.riv,在 Linux 上运行 sha256sum your-file.riv。
网络请求、URL 与导航(Network requests, URLs, and navigation)
脚本(Scripts)
没有任何 Rive 运行时向脚本提供网络访问。
Hosted 资源(Hosted assets)
在 Rive 编辑器中,你可以把图像、字体或音频资源设为 Hosted。Hosted 资源留在 Rive 的 CDN 上,而不是写进 .riv 文件;部分运行时在加载文件时会下载它。Web 运行时以及旧版 Apple 和 Android 运行时,会从文件中存储的地址下载,因此被改动的文件可以把下载指向任意服务器。脚本不能更改该地址,也不能向请求附加数据。
| 运行时 | 默认是否下载 Hosted 资源 | 如何关闭 |
|---|---|---|
| Apple,新运行时(New Runtime) | 否 | 无需操作 |
| Apple,旧版运行时(Legacy Runtime) | 是 | 向 RiveFile 或 RiveViewModel 传入 loadCdn: false |
| Android,新运行时 | 否 | 无需操作 |
| Android,旧版运行时 | 是 | 在 RiveAnimationView builder 上调用 setShouldLoadCDNAssets(false),在 XML 中设置 app:riveShouldLoadCDNAssets="false",或创建 FallbackAssetLoader 时设 loadCDNAssets = false |
| Web | 是 | 在 Rive 选项中设置 enableRiveAssetCDN: false |
其他运行时请参阅对应运行时的「加载资源」页面。
Open URL 事件(Open URL events)
Apple 和 Android 运行时不会打开来自文件的 URL。如果你的代码在监听事件,它会把 Open URL 事件 作为数据接收,并由你决定如何处理。Web 运行时只有在把已弃用的 automaticallyHandleEvents 选项设为 true 时才会打开这些 URL。默认值为 false。
导航(Navigation)
Rive 运行时不能导航你的应用或打开屏幕。文件只能通过你的代码读取的事件和视图模型值来影响应用。如果你的代码根据这些值导航、打开 URL 或调用你的服务器,请先检查它们,就像对待来自应用外部的任何输入一样。
关闭脚本(Turning off scripting)
Rive 运行时没有针对单个文件或整个应用关闭脚本的设置。若要在不运行脚本的情况下播放 Rive 文件,请使用去掉脚本的运行时构建。这些构建不运行 AnimaScript、Luau 或着色器。文件仍会加载并播放,但由脚本驱动的任何内容都会停止工作。
| 平台 | 不含脚本的运行时 |
|---|---|
| Web | 使用 @rive-app/canvas-lite 或 @rive-app/react-canvas-lite。这些包还会去掉文本、布局、音频和矢量羽化。 |
| Apple | 从源码构建 rive-ios,并加上 --no-scripting 选项。参见其 contributing 指南。 |
| Android | 从源码构建 rive-android,并加上 noScripting Gradle 属性,例如 -PnoScripting。 |
处理敏感数据的应用建议(Recommendations for apps that handle sensitive data)
- 控制文件来源。 能打包进应用就打包,并在加载前检查每一个下载的文件。
- 只绑定文件需要的数据。 脚本可以读取你的应用绑定到其文件的每一个视图模型属性。永远不要把密码、令牌或完整卡号放进视图模型。
- 密钥用原生文本框。 脚本可以读取用户在其文件的文本输入(Text Input) 中键入的内容。
- 不要把秘密放进文件。 .riv 文件未加密。拿到文件的人可以检查其内容,包括已编译脚本以及其中的任何文本。
- 把文件回传的内容当作不可信输入。 在根据事件和视图模型值采取行动之前先检查它们。对于 URL 和屏幕,只接受你定义的列表中的值。
- 关掉你不用的能力。 如果不用 Hosted 资源,就关闭其下载。在 Web 上保持
automaticallyHandleEvents关闭。如果文件没有脚本,使用不含脚本的运行时。 - 在 Web 上要求 fuel 检查。 如果你的 Web 应用播放来自团队外部的 AnimaScript 文件,请要求用
wasmFuel: true构建的文件,以便浏览器能停下失控脚本。除非你信任来源,否则不要交付用dangerouslyFast构建的文件。 - 保持 Rive 运行时为最新,以获得安全修复。
安全资源(Security resources)
- 开源运行时。 Rive 运行时以 MIT 许可证开源,你的团队可以审阅应用实际运行的代码:rive-runtime(C++ 核心,包括签名检查和两套脚本引擎)、rive-ios、rive-android 和 rive-wasm。
- AnimaScript。 Where Scripts Run 说明 AnimaScript 在各平台如何运行,以及时间预算如何工作。
- SOC 2 Type II。 Enterprise 计划 的一部分。
- 报告漏洞。 请通过支持表单私下联系我们。请不要在社区论坛或 GitHub issues 中报告安全问题。