五个真实漏洞案例揭示GitHub私有报告的必要性

本文通过五个真实漏洞案例详细分析了漏洞披露过程中的挑战,包括Rust库中的未定义行为、DoS向量、加密标签长度限制缺失等问题,并介绍了GitHub私有报告功能如何简化披露流程。

5个追求更好披露流程的理由 - Trail of Bits博客

本博客展示了我们在过去一年中披露的五个真实漏洞案例(此前未公开披露)。我们还分享了在披露这些漏洞时遇到的挫折,以说明有效披露流程的必要性。

以下是五个漏洞:

  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文档)。如果依赖存储库启用了Dependabot,那么对您项目的依赖会自动更新。

除此之外,GitHub还可以自动发布CVE ID,可用于在GitHub之外引用错误。

此私有报告功能在GitHub上仍处于官方测试阶段。我们遇到了小问题,如缺少消息模板和报告者无法添加协作者。我们将后者作为错误报告给GitHub,但他们声称这是设计使然。

步骤3:通过webhooks获取通知

如果您希望在选择的 messaging 平台(如Slack)中接收通知,可以在GitHub上创建存储库或组织范围的webhook。只需启用以下事件类型:

创建webhook后,repository_advisory事件将发送到设置的webhook URL。该事件包括报告漏洞的摘要和描述。

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