此前的 无所畏惧的安全系列文章 探讨了 Rust 中的 内存安全 和 线程安全。这篇收尾文章以 Quantum CSS 项目 为案例研究,探讨了用 Rust 重写代码的现实影响。
样式组件是浏览器中将 CSS 规则应用于页面的部分。这是一个在 DOM 树上自上而下的过程:在给定父级样式的情况下,可以独立计算子级的样式,这非常适合并行计算。到 2017 年,Mozilla 已经尝试过两次使用 C++ 并行化样式系统,但都失败了。
Quantum CSS 源于提高页面性能的需求,而提升安全则是额外的收获。
内存安全违规与安全相关错误之间有很大重叠,因此我们预计这次重写将减少 Firefox 中的攻击面。在这篇文章中,我将总结自 Firefox 于 2002 年首次发布 以来,样式代码中出现的潜在安全漏洞。然后,我将分析使用 Rust 能否以及如何防止这些漏洞。
在 Firefox 的样式组件的生命周期中,共出现了 69 个安全漏洞。如果我们有一台时间机器,可以从一开始就用 Rust 编写这个组件,其中 51 个(73.9%)漏洞就不会出现。虽然 Rust 使得编写更优质的代码变得更容易,但它并非万无一失。
Rust
Rust 是一种现代系统编程语言,具有类型安全和内存安全。作为这些安全保证的副作用,Rust 程序也以在编译时线程安全而闻名。因此,当
✅ 安全地处理不可信输入。
✅ 引入并行性以提高性能。
✅ 将隔离的组件集成到现有代码库中。
时,Rust 可能是特别好的选择。然而,有一些类型的错误是 Rust 明确不处理的,特别是正确性错误。事实上,在 Quantum CSS 重写过程中,工程师意外地重新引入了 C++ 代码中先前已修复的一个关键安全错误,导致 错误 641731 的修复失效。这允许通过 SVG 图像文档泄露全局历史记录,导致出现 错误 1420001。作为一个简单的历史窃取错误,其安全等级被评为高。原始修复是在使用 SVG 文档作为图像之前进行额外检查。不幸的是,在重写过程中忽略了此检查。
虽然我们有旨在捕获类似此类 :visited
规则违规的自动化测试,但实际上它们并没有检测到此错误。为了加快自动化测试速度,我们暂时关闭了测试此功能的机制,如果测试没有运行,就没什么用。重新实现逻辑错误的风险可以通过良好的测试覆盖率(以及实际运行测试)来降低。仍然存在引入新的逻辑错误的风险。
随着开发人员对 Rust 语言的熟悉程度提高,最佳实践也将得到改进。用 Rust 编写的代码将变得更加安全。虽然它可能无法防止所有可能的漏洞,但 Rust 消除了最严重错误的整个类别。
Quantum CSS 安全漏洞
总的来说,与内存、边界、空/未初始化变量或整数溢出相关的错误在 Rust 中默认会被阻止。我上面提到的杂项错误不会被阻止,它是由分配失败导致的崩溃。
按类别划分的安全漏洞
本分析中所有的漏洞都与安全相关,但只有 43 个获得了官方安全分类。(这些分类由 Mozilla 的安全工程师根据对“可利用性”的判断而分配。)普通的错误可能表明缺少功能或问题,例如崩溃。虽然不可取,但崩溃不会导致数据泄露或行为修改。官方的安全漏洞的严重程度从低(范围非常有限)到严重漏洞(可能允许攻击者在用户的平台上运行任意代码)不等。
内存漏洞和严重的安全问题之间存在很大的重叠。在 34 个严重/高等级漏洞中,有 32 个与内存相关。
按安全等级分类的漏洞
比较 Rust 和 C++ 代码
错误 955914 是 GetCustomPropertyNameAt
函数中的堆缓冲区溢出。代码使用了错误的变量进行索引,导致解释数组末尾之后的内存。这会导致在访问错误的指针时崩溃,或者将内存复制到传递给另一个组件的字符串中。
所有 CSS 属性(包括长属性和自定义属性)的顺序都存储在数组 mOrder
中。每个元素要么由其 CSS 属性值表示,要么在自定义属性的情况下,由从 eCSSProperty_COUNT
(非自定义 CSS 属性的总数)开始的值表示。要检索自定义属性的名称,首先必须从 mOrder
中检索自定义属性值,然后访问 mVariableOrder
数组中对应索引处的名称,该数组按顺序存储自定义属性名称。
有漏洞的 C++ 代码
void GetCustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const {
MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT);
aResult.Truncate();
aResult.AppendLiteral("var-");
aResult.Append(mVariableOrder[aIndex]);
问题出现在第 6 行,使用 aIndex
访问 mVariableOrder
数组的元素。aIndex
旨在与 mOrder
数组一起使用,而不是与 mVariableOrder
数组一起使用。mOrder
中由 aIndex
表示的自定义属性的对应元素实际上是 mOrder[aIndex] - eCSSProperty_COUNT
。
已修复的 C++ 代码
void Get CustomPropertyNameAt(uint32_t aIndex, nsAString& aResult) const {
MOZ_ASSERT(mOrder[aIndex] >= eCSSProperty_COUNT);
uint32_t variableIndex = mOrder[aIndex] - eCSSProperty_COUNT;
aResult.Truncate();
aResult.AppendLiteral("var-");
aResult.Append(mVariableOrder[variableIndex]);
}
等效的 Rust 代码
虽然 Rust 在某些方面类似于 C++,但习惯性的 Rust 使用了不同的抽象和数据结构。Rust 代码与 C++ 代码看起来非常不同(有关详细信息,请参见下文)。首先,让我们考虑一下如果我们尽可能地直译有漏洞的代码会发生什么
fn GetCustomPropertyNameAt(&self, aIndex: usize) -> String {
assert!(self.mOrder[aIndex] >= self.eCSSProperty_COUNT);
let mut result = "var-".to_string();
result += &self.mVariableOrder[aIndex];
result
}
Rust 编译器会接受代码,因为没有办法在运行时确定向量的长度。与必须知道其长度的数组不同,Rust 中的 Vec
类型 是动态大小的。然而,标准库向量实现具有内置的边界检查功能。当使用无效索引时,程序会以受控的方式立即终止,防止任何非法访问。
Quantum CSS 中的 实际代码 使用了非常不同的数据结构,因此没有完全等效的代码。例如,我们使用 Rust 中强大的内置数据结构来统一排序和属性名称数据。这样一来,我们就不需要维护两个独立的数组。Rust 数据结构还改善了数据封装,并减少了这类逻辑错误的可能性。由于代码需要与浏览器引擎中其他部分的 C++ 代码进行交互,因此新的 GetCustomPropertyNameAt
函数看起来不像习惯性的 Rust 代码。它仍然提供所有安全保证,同时提供更易于理解的底层数据抽象。
tl;dr;
由于内存安全违规与安全相关错误之间存在重叠,我们可以说 Rust 代码应该会产生更少的关键 CVE(常见漏洞和披露)。然而,即使是 Rust 也并非万无一失。开发人员仍然需要意识到正确性错误和数据泄露攻击。代码审查、测试和模糊测试对于维护安全的库仍然至关重要。
编译器无法捕获程序员可能犯下的所有错误。但是,Rust 的设计消除了内存安全的负担,使我们能够专注于逻辑正确性和健壮性。
16 条评论