5个理由推动更优漏洞披露流程:真实案例与技术解析

本文通过5个真实漏洞案例(包括Rust库未定义行为、以太坊ABI解析DoS、Expo认证标签长度限制缺失、num-bigint反序列化漏洞、React Native密钥泄露),深入分析漏洞技术细节与披露挑战,并详解GitHub私有报告功能如何优化安全协作流程。

5个理由推动更优漏洞披露流程

本文展示了我们在过去一年中披露的5个真实漏洞案例(此前未公开),并分享了披露过程中遇到的挫折,以此说明建立有效披露流程的必要性。以下是五个漏洞:

  1. borsh-rs Rust库中的未定义行为
  2. 解析以太坊ABI的Rust库中的拒绝服务(DoS)向量
  3. Expo中认证标签长度限制缺失
  4. num-bigint Rust库中的DoS向量
  5. react-native-mmkv将MMKV数据库加密密钥插入Android系统日志

在开源项目中发现漏洞需要谨慎处理,因为公开报告(即完全披露)可能在修复准备完成前就提醒攻击者。协调漏洞披露(CVD)采用更安全的结构化报告框架以最小化风险。我们的五个案例表明,缺乏CVD流程会不必要地复杂化漏洞报告并影响及时修复。

在“关键要点”部分,我们通过提供可用的基本安全策略和逐步介绍简化的披露流程(称为GitHub私有报告),指导您如何为项目成功设置安全措施。GitHub此功能具有多个优势:

  • 对开发者的隐蔽安全警报:无需PGP加密邮件
  • 流程简化:无需与企业邮箱地址玩捉迷藏
  • 简化CVE发布:无需向MITRE提交CVE表格

行动时机:如果您拥有GitHub上的知名项目,请立即使用私有报告!阅读更多关于为仓库配置私有漏洞报告,或直接跳至本文的“关键要点”部分。

案例1:borsh-rs Rust库中的未定义行为

第一个案例涉及加密序列化库borsh-rs中的一个漏洞,该漏洞两年未修复,这也是实施全面安全策略的原因。

在审计期间,我发现了不安全的Rust代码,当与未实现Copy特性的零大小类型一起使用时,可能导致未定义行为。尽管之前有人报告过此漏洞,但由于开发人员不清楚如何避免代码中的未定义行为并保持相同属性(例如抗DoS攻击),它一直未被修复。在此期间,库用户未被告知此漏洞。

使用GitHub的私有报告功能可以简化整个流程。如果项目开发人员无法在私下报告时处理漏洞,他们仍可以一键通知Dependabot用户。在GitHub上私下报告漏洞时,发布实际修复是可选的。

我联系了borsh-rs开发人员关于在没有可用修复时通知用户的问题。开发人员认为最好通知用户,因为只有库的某些使用会导致未定义行为。我们提交了通知RUSTSEC-2023-0033,并创建了GitHub咨询。几个月后,开发人员修复了漏洞,并发布了主要版本1.0.0。随后我更新了RustSec咨询以反映修复情况。

以下代码包含导致未定义行为的漏洞:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
impl<T> BorshDeserialize for Vec<T>
where
    T: BorshDeserialize,
{
    #[inline]
    fn deserialize<R: Read>(reader: &mut R) -> Result<Self, Error> {
        let len = u32::deserialize(reader)?;
        if size_of::<T>() == 0 {
            let mut result = Vec::new();
            result.push(T::deserialize(reader)?);

            let p = result.as_mut_ptr();
            unsafe {
                forget(result);
                let len = len as usize;
                let result = Vec::from_raw_parts(p, len, len);
                Ok(result)
            }
        } else {
            // TODO(16): return capacity allocation when we can safely do that.
            let mut result = Vec::with_capacity(hint::cautious::<T>(len));
            for _ in 0..len {
                result.push(T::deserialize(reader)?);
            }
            Ok(result)
        }
    }
}

图1:不安全Rust的使用(borsh-rs/borsh-rs/borsh/src/de/mod.rs#123–150)

图1中的代码将字节反序列化为某种泛型数据类型T的向量。如果类型T是零大小类型,则会执行不安全的Rust代码。代码首先将向量的请求长度读取为u32。之后,代码分配一个空的Vec类型。然后它将T的单个实例推入其中。随后,通过调用forget函数临时泄漏刚分配Vec的内存,并通过将Vec的长度和容量设置为请求长度来重建它。因此,不安全的Rust代码假设T是可复制的。

不安全的Rust代码防止了DoS攻击,其中反序列化的内存表示明显大于序列化的磁盘表示。攻击通过将向量长度设置为大数并使用零大小类型来工作。此漏洞的一个实例在我们的博客文章Billion times emptiness中描述。

案例2:解析以太坊ABI的Rust库中的DoS向量

7月,我披露了四个以太坊API解析库中的多个DoS漏洞,这些漏洞难以报告,因为我必须联系多个相关方。

该漏洞影响了四个GitHub托管项目。只有Python项目eth_abi启用了GitHub私有报告。对于其他三个项目(ethabi、alloy-rs和ethereumjs-abi),我必须研究谁在维护它们,这容易出错。例如,我不得不通过向GitHub提交URL追加后缀.patch来获取维护者的电子邮件地址。以下链接显示了我用于提交的非工作电子邮件地址:

https://github.com/trailofbits/publications/commit/a2ab5a1cab59b52c4fa71b40dae1f597bc063bdf.patch

有关此漏洞技术细节的更多信息,请阅读博客文章Billion times emptiness

案例3:Expo中认证标签长度限制缺失

2022年底,Trail of Bits的安全工程师Joop van de Pol在expo-secure-store中发现了一个加密漏洞。在这种情况下,供应商Expo未能跟进我们关于是否确认或修复漏洞的反馈,使我们处于黑暗中。更糟糕的是,尝试跟进供应商消耗了大量本可以用于在开源软件中查找更多漏洞的时间。

当我们最初通过其GitHub上列出的电子邮件地址secure@expo.io向Expo发送关于漏洞的邮件时,一名Expo员工在一天内回复并确认会将报告转发给他们的技术团队。然而,在那次回复之后,尽管在一年内进行了两次温和的提醒,我们再也没有收到Expo的回复。

不幸的是,Expo不允许通过GitHub进行私有报告,因此电子邮件是我们唯一的联系地址。

现在来看漏洞的具体细节:在API级别23以上的Android上,SecureStore使用KeyStore中的AES-GCM密钥加密存储值。在加密过程中,标签长度和初始化向量(IV)由底层Java加密库作为Cipher类的一部分生成,并与密文一起存储:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/* package */ JSONObject createEncryptedItem(Promise promise, String plaintextValue, Cipher cipher, GCMParameterSpec gcmSpec, PostEncryptionCallback postEncryptionCallback) throws GeneralSecurityException, JSONException {

  byte[] plaintextBytes = plaintextValue.getBytes(StandardCharsets.UTF_8);
  byte[] ciphertextBytes = cipher.doFinal(plaintextBytes);
  String ciphertext = Base64.encodeToString(ciphertextBytes, Base64.NO_WRAP);

  String ivString = Base64.encodeToString(gcmSpec.getIV(), Base64.NO_WRAP);
  int authenticationTagLength = gcmSpec.getTLen();

  JSONObject result = new JSONObject()
    .put(CIPHERTEXT_PROPERTY, ciphertext)
    .put(IV_PROPERTY, ivString)
    .put(GCM_AUTHENTICATION_TAG_LENGTH_PROPERTY, authenticationTagLength);

  postEncryptionCallback.run(promise, result);

  return result;
}

图2:加密存储项的代码,其中标签长度与密文一起存储(SecureStoreModule.java)

对于解密,读取密文、标签长度和IV,然后使用KeyStore中的AES-GCM密钥进行解密。

能够访问存储的攻击者可以将现有的AES-GCM密文改为具有更短的认证标签。根据底层Java加密服务提供商的实现,最佳情况下最小标签长度为32位(这是NIST规范允许的最小值),但在最坏情况下可能更低(例如8位甚至1位)。因此在最佳情况下,攻击者修改密文后相同标签被接受的概率很小但不可忽略,而在最坏情况下,这个概率可能很大。无论哪种情况,成功概率都取决于密文块的数量。此外,重复的解密失败和成功最终都会泄露认证密钥。有关如何执行此攻击的详细信息,请参阅NIST的GCM中的认证弱点

从加密角度来看,这是一个问题。然而,由于需要存储访问,在实践中利用此问题可能很困难。基于我们的发现,我们建议将标签长度固定为128位,而不是写入存储并从那里读取。

故事本应在这里结束,因为我们在初始交流后没有收到Expo的任何回复。但在我们的第二次电子邮件提醒中,我们提到我们将公开披露此问题。一周后,漏洞通过将最小标签长度限制为96位被静默修复。实际上,96位提供了足够的安全性。然而,也没有理由不选择更高的128位。

修复是在我们最后一次提醒后整整一周创建的。我们怀疑我们之前的电子邮件提醒导致了修复,但我们不确定。不幸的是,我们从未得到适当的认可。

案例4:num-bigint Rust库中的DoS向量

2023年7月,Trail of Bits的安全工程师Sam Moelius在著名的num-bigint Rust库中遇到了一个DoS向量。尽管通过电子邮件的披露工作非常顺利,但用户从未通过GitHub咨询或CVE等方式被告知此漏洞。

num-bigint项目托管在GitHub上,但未设置GitHub私有报告,因此库作者或我们都没有快速创建咨询的方式。Sam通过发送电子邮件向num-bigint的开发人员报告了此漏洞。但查找开发人员的电子邮件容易出错且耗时。您必须首先通过电子邮件确认已联系到正确的人,然后才发送漏洞详细信息。使用GitHub私有报告或仓库中的安全策略,发送漏洞的渠道将很清晰。

但现在让我们讨论漏洞本身。该库实现了非常大的整数,这些整数不再适合原始数据类型如i128。除此之外,库还可以序列化和反序列化这些数据类型。Sam发现的漏洞隐藏在该序列化功能中。具体来说,库可能由于大量内存消耗或请求的内存分配过大而失败而崩溃。

num-bigint类型实现了Serde的特性。这意味着crate中的任何类型都可以使用任意文件格式(如JSON或bincode crate使用的二进制格式)进行序列化和反序列化。以下示例程序展示了如何使用此反序列化功能:

1
2
3
4
5
6
7
8
9
use num_bigint::BigUint;
use std::io::Read;

fn main() -> std::io::Result<()> {
    let mut buf = Vec::new();
    let _ = std::io::stdin().read_to_end(&mut buf)?;
    let _: BigUint = bincode::deserialize(&buf).unwrap_or_default();
    Ok(())
}

图3:示例反序列化格式

事实证明,某些输入会导致上述程序崩溃。这是因为实现Visitor特性使用不受信任的用户输入来分配特定的向量容量。下图显示了可能导致程序崩溃并显示消息“memory allocation of 2893606913523067072 bytes failed”的代码行:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
impl<'de> Visitor<'de> for U32Visitor {
    type Value = BigUint;

    {...为简洁省略...}

    #[cfg(not(u64_digit))]
    fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
    where
        S: SeqAccess<'de>,
    {
        let len = seq.size_hint().unwrap_or(0);
        let mut data = Vec::with_capacity(len);

        {...为简洁省略...}
    }

    #[cfg(u64_digit)]
    fn visit_seq<S>(self, mut seq: S) -> Result<Self::Value, S::Error>
    where
        S: SeqAccess<'de>,
    {
        use crate::big_digit::BigDigit;
        use num_integer::Integer;

        let u32_len = seq.size_hint().unwrap_or(0);
        let len = Integer::div_ceil(&u32_len, &2);
        let mut data = Vec::with_capacity(len);

        {...为简洁省略...}
    }
}

图4:基于用户输入分配内存的代码(num-bigint/src/biguint/serde.rs#61–108)

我们于2023年7月20日首次联系作者,漏洞在2023年8月22日的提交44c87c1中修复。修复版本于次日发布为0.4.4。

案例5:react-native-mmkv将MMKV数据库加密密钥插入Android系统日志

最后一个案例涉及react-native-mmkv库中明文加密密钥的披露,该漏洞于2023年9月修复。在为客户进行安全代码审查期间,我发现了一个提交修复了关键依赖项中未跟踪的漏洞。由于没有安全咨询或CVE ID,我和客户都未被告知该漏洞。漏洞管理的缺乏导致攻击者知道漏洞而用户却蒙在鼓里的情况。

在客户参与期间,我想验证加密密钥的使用和处理方式。提交修复:“Don’t leak encryption key in logs”在react-native-mmkv库中引起了我的注意。以下代码显示了有问题的日志语句:

1
2
3
4
5
6
7
MmkvHostObject::MmkvHostObject(const std::string& instanceId, std::string path,
                               std::string cryptKey) {
  __android_log_print(ANDROID_LOG_INFO, "RNMMKV",
                      "Creating MMKV instance \"%s\"... (Path: %s, Encryption-Key: %s)",
                      instanceId.c_str(), path.c_str(), cryptKey.c_str());
  std::string* pathPtr = path.size() > 0 ? &path : nullptr;
  {...为简洁省略...}

图5:初始化MMKV并记录加密密钥的代码

在该修复之前,我正在调查的加密密钥以明文形式打印到Android系统日志中。这破坏了威胁模型,因为即使启用了Android调试功能,也不应从设备中提取此加密密钥。

经客户同意,我通知了react-native-mmkv的作者,作者和我得出结论,应通知库用户有关漏洞。因此作者启用了私有报告,我们一起发布了GitHub咨询。漏洞被分配了ID CVE-2024-21668。该咨询现在会在开发人员运行npm audit或npm install时使用易受攻击版本的react-native-mmkv时发出警报。

此案例突出表明,在npm包方面,基本上没有绕过GitHub咨询的方法。填充npm audit命令输出的唯一方法是创建GitHub咨询。使用私有报告简化了该过程。

关键要点

GitHub的私有报告功能有助于保护软件生态系统。如果使用正确,该功能为漏洞报告者和软件维护者节省时间。私有报告的最大影响是它与GitHub咨询数据库相关联——这种联系在例如使用GitLab的机密问题时是缺失的。通过GitHub的私有报告功能,安全研究人员现在有一个流程可以发布到该数据库(经仓库维护者批准)。

在GitHub上使用私有报告也使披露过程更加清晰。使用电子邮件时,不清楚是否应该加密邮件以及应该发送给谁。如果您曾经加密过电子邮件,您知道有无尽的陷阱。

然而,您可能仍然希望向开发人员或安全联系人发送电子邮件通知,因为维护者可能会错过GitHub通知。包含已创建咨询链接的基本电子邮件通常足以提高意识。

步骤1:添加安全策略

发布安全策略是拥有漏洞报告流程的第一步。为避免混淆,良好的策略明确定义了发现漏洞时应采取的措施。

GitHub有两种发布安全策略的方式。您可以在仓库根目录创建SECURITY.md文件,或者通过创建.github仓库并在其根目录放置SECURITY.md文件来创建用户或组织范围的策略。

我们建议从使用disclose.io的Policymaker生成的策略开始(参见此示例),但将“Official Channels”部分替换为以下内容:

我们有多个接收报告的渠道:

  • 如果您发现特定GitHub项目的任何安全问题,请点击相关GitHub项目“Security”选项卡上的“Report a vulnerability”按钮:https://github.com/%5BYOUR_ORG%5D/%5BYOUR_PROJECT%5D。
  • 发送电子邮件至security@example.com

始终确保至少包含两个联系点。如果一个失败,报告者仍有另一个选项,然后再回退到直接联系开发人员。

步骤2:启用私有报告

现在安全策略已设置,查看引用的GitHub私有报告功能,该工具允许 discreetly 向维护者沟通漏洞,以便他们在公开披露之前修复问题。它还通知更广泛的社区,例如npm、Crates.io或Go用户,关于其依赖项中潜在的安全问题。

启用和使用该功能很容易,几乎不需要维护。唯一的关键是确保正确设置GitHub通知。报告仅通过电子邮件发送如果您配置了电子邮件通知。默认未启用此功能的原因是它需要主动监控GitHub通知,否则报告可能得不到所需的关注。

配置通知后,转到仓库的“Security”选项卡并点击“Enable vulnerability reporting”:

关于报告漏洞的电子邮件主题为“(org/repo) Summary (GHSA-0000-0000-0000)。”如果您使用网站通知,您将收到类似这样的通知:

如果您想为整个组织启用私有报告,请查看此文档

使用私有报告的一个好处是漏洞发布在GitHub咨询数据库中(参见[GitHub文档](https://

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计