五大漏洞披露案例:GitHub私有报告如何优化安全流程

本文通过五个真实漏洞案例,深入分析漏洞披露过程中的挑战与解决方案,重点介绍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攻击,其中反序列化的内存表示明显大于序列化的磁盘表示。攻击通过将向量长度设置为大数并使用零大小类型来工作。此漏洞的一个实例在我们的博客文章十亿次空虚中描述。

案例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

在博客文章十亿次空虚中阅读更多关于此漏洞的技术细节。

案例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特性使用不受信任的用户输入来分配特定的向量容量。下图显示了可能导致程序崩溃并显示消息“内存分配2893606913523067072字节失败”的代码行:

 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 in the react-native-mmkv library引起了我的注意。以下代码显示了有问题的日志语句:

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上的私有报告,披露过程也变得 clearer。使用电子邮件时,不清楚是否应该加密电子邮件以及应该发送给谁。如果您曾经加密过电子邮件,您知道有无尽的陷阱。

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

步骤1:添加安全策略

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

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

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

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

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

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

步骤2:启用私有报告

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

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

配置通知后,转到存储库的“安全”选项卡并点击“启用漏洞报告”:

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

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

使用私有报告的一个好处是漏洞发布在GitHub咨询数据库中(参见GitHub文档以获取更多信息)。如果依赖存储库启用了Dependabot,那么对您项目的依赖会自动更新。

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

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