安全性考量#
本文档描述了从不受信任的来源读取 Arrow 数据时的安全性考量。重点针对以标准化序列化形式(如 IPC 流)传递的数据,而非特定于实现的原生表示(如 C++ 中的 arrow::Array)。
重要提示
特定于实现的疑虑(例如 API 使用不当)不在本文档讨论范围内。请参考各实现自身的文档。
另请参阅
- Arrow C++ 安全性考量
Arrow C++ API 的安全模型
谁应该阅读本文档#
如果您属于以下两类人群之一,则应阅读本文档:
Arrow 的用户:即第三方库或应用程序的开发者,他们不直接实现 Arrow 格式或协议,而是调用由 Arrow 库(定义如下)提供的语言特定 API;
Arrow 库的实现者:即提供抽象了 Arrow 格式和协议细节的 API 的库;此类库包括但不限于 https://arrow.apache.org 上记录的官方 Arrow 实现。
列式格式#
无效数据#
Arrow 列式格式 是一种专注于性能和效率的高效二进制表示形式。虽然该格式不存储原始指针,但 Arrow 缓冲区的内容经常被组合并转换为指向进程地址空间的指针。因此,无效的 Arrow 数据可能会导致无效的内存访问(可能导致进程崩溃)或访问非 Arrow 数据(可能允许攻击者窃取机密信息)。
例如,要从 Binary 数组读取值,需要 1) 从数组的偏移缓冲区读取值的偏移量,以及 2) 读取数组数据缓冲区中由这些偏移量界定的字节范围。如果偏移量无效(无论是否蓄意),则第 2 步可能会访问数据缓冲区范围之外的内存。
无效数据的另一个实例在于值本身。例如,String 数组仅允许包含有效的 UTF-8 数据,但不受信任的来源可能伪装成 String 数组并发布无效的 UTF-8 数据。一个仅针对有效 UTF-8 输入设计的算法可能会导致危险行为(例如,在寻找 UTF-8 字符边界时越界读取内存)。
幸运的是,如果知道模式(Schema),就可以预先验证 Arrow 数据,从而确保后续读取该数据不会造成任何危险。
给用户的建议#
Arrow 实现通常假设输入遵循规范以提供高速处理。强烈建议您的应用程序显式验证其从不受信任来源接收的任何序列化形式的 Arrow 数据。许多 Arrow 实现提供了专门的 API 来执行此类验证。
给实现者的建议#
建议您提供专门的 API 来验证 Arrow 数组和/或记录批次(Record Batches)。用户将能够利用这些 API 来断言来自不受信任来源的数据是否可以安全访问。
典型的验证 API 必须返回明确定义的错误,而不能崩溃(如果给定的 Arrow 数据无效);无论数据是否有效,执行该 API 都必须始终是安全的。
未初始化的数据#
一个不太明显的陷阱是当 Arrow 数组的某些部分未初始化时。例如,如果原始 Arrow 数组的某个元素通过其有效性位图(validity bitmap)标记为 null,则值缓冲区中相应的值槽(slot)在任何用途中都可以忽略。因此,在创建带有 null 值的数组时,不初始化相应的值槽是很诱人的做法。
然而,如果 Arrow 数据被序列化并发布(例如使用 IPC 或 Flight),使得不受信任的用户可以访问它,这将引入严重的安全性风险。实际上,未初始化的值槽可能会泄露同一进程中先前内存分配遗留的数据。根据应用程序的不同,这些数据可能包含机密信息。
给用户和实现者的建议#
在创建 Arrow 数组时,建议如果数组可能发送给不受信任的第三方或被其读取,则切勿在缓冲区中留下任何未初始化的数据,即使未初始化的数据在逻辑上是无关紧要的。最简单的做法是将任何未填满的缓冲区进行零初始化。
如果通过基准测试确定零初始化带来了过高的性能成本,库或应用程序可以选择在内部将未初始化内存作为一种优化手段;但之后必须确保在将 Arrow 数据传递给另一个系统之前清除所有此类未初始化值。
注意
将 Arrow 数据发送到当前进程之外的情况可能会间接发生,例如,如果您通过 C Data Interface 生成它,而消费者使用 IPC 格式将其持久化到某些公共存储中。
C 数据接口#
C Data Interface 包含指向进程地址空间的原始指针。通常无法验证这些指针是否合法;从这样的指针读取数据可能会导致崩溃或访问无关的错误数据。
给用户的建议#
您绝不应该使用来自不受信任生产者的 C Data Interface 结构,因为在这种情况下,从构造上讲是不可能防范危险行为的。
给实现者的建议#
在使用 C Data Interface 结构时,由于上述原因,您可以假设它来自可信的生产者。然而,仍然建议您验证其稳健性(例如,针对给定的数据类型传递了正确数量的缓冲区),因为可信生产者也可能存在 bug。
IPC 格式#
IPC 格式 是一种带有相关元数据的列式格式序列化格式。从不受信任的来源读取 IPC 流或文件与读取 Arrow 列式格式具有类似的注意事项。
IPC 格式中的额外信号和元数据也有其自身的风险。例如,IPC 消息中编码的缓冲区偏移量和大小可能会超出 IPC 流的范围;Flatbuffers 编码的元数据负载可能带有错误的偏移量,指向指定元数据区域之外。
给用户的建议#
Arrow 库通常会确保 IPC 流在结构上有效,但可能不会验证底层的 Array 数据。强烈建议您使用适当的 API 来验证从不受信任的 IPC 流读取的 Arrow 数据。
给实现者的建议#
强烈建议在解码 IPC 格式时运行专门的验证检查,以确保解码不会引起不必要的行为。如果这些检查失败,应向调用者返回明确的错误,而不是崩溃。
扩展类型#
扩展类型(Extension types)通常会注册自定义反序列化钩子(deserialization hook),以便在从外部源读取(例如使用 IPC)时能够自动重建它们。反序列化钩子必须从该扩展类型特有的字符串或二进制负载中解码扩展类型的参数。典型示例使用定制的 JSON 表示形式,其中对象字段代表各种参数。
当从不受信任的来源读取数据时,任何已注册的反序列化钩子都可能被任意负载调用。因此,最重要的是确保钩子在调用无效或潜在恶意数据时是安全的。这要求使用健壮的元数据序列化模式(例如 JSON,而不是例如 Python 的 pickle 或 R 的 serialize())。
给用户和实现者的建议#
在设计扩展类型时,强烈建议选择一种对潜在恶意数据具有健壮性的元数据序列化格式。
在实现扩展类型时,建议确保反序列化钩子能够在序列化元数据负载无效时检测到并优雅地报错。
健壮性测试#
给实现者的建议#
对于可能处理不受信任输入的 API,强烈建议您的单元测试针对典型的无效数据测试您的 API。例如,您的验证 API 将必须针对无效的 Binary 或 List 偏移量、String 数组中的无效 UTF-8 数据等进行测试。
针对已知回归文件进行测试#
arrow-testing 仓库包含各种格式(如 IPC 格式)的回归文件。
有两类文件特别值得注意,可以用来检验 Arrow 实现的健壮性:
模糊测试(Fuzzing)#
建议您更进一步,建立某种针对不可预见输入的自动化健壮性测试。一种典型的方法是模糊测试,可能结合检测危险行为的运行时检测框架(如 C++ 或 Rust 中的地址消毒器 Address Sanitizer)。
建立 Arrow 模糊测试的一种合理方法是使用 IPC 格式作为二进制负载;模糊测试目标不仅应尝试将 IPC 流解码为 Arrow 数据,还应验证该 Arrow 数据。这将加强 IPC 解码器和验证例程以应对无效且可能恶意的输入。最后,如果验证成功,模糊测试目标可以运行一些重要的核心功能,例如将数据打印以供人类查看;这将有助于确保验证例程没有放过可能导致危险行为的无效数据。
非 Arrow 格式和协议#
Arrow 数据也可以使用第三方格式(如 Apache Parquet)发送或存储。这些格式可能存在也可能不存在上述列出的安全风险(例如,有关未初始化数据的预防措施可能不适用于 Parquet 这样不为 null 元素创建值槽的格式)。我们建议您参考这些项目自身的文档以获取更具体的指导。