开源软件的安全问题理所当然地引起了业界的关注,但解决方案需要对执行过程中的挑战和合作达成共识。这个问题很复杂,涉及诸多方面:供应链、依赖库管理、身份识别和构建流水线(build pipelines)。如果能够准确定义问题,便可更快得出解决方案;为此,我们提出了一个框架("知悉、预防、修复"),该框架是关于行业内应当如何考虑开源软件中的漏洞,并列举了需要首先解决的具体问题,包括:
就元数据和身份识别标准达成共识。我们需要在基本面上达成一致来解决这些行业复杂问题。就元数据细节和身份识别达成的共识将有助于实现自动化,减少更新软件所需的工作量,并将漏洞的影响最小化。
提高关键软件的透明度并加强代码审查。对于安全关键性软件,我们需要就开发流程达成一致意见,确保充分的审查,避免单方面变更,并透明公开地生成定义明确、可被验证的正式版本。
下面提出的框架和目标,旨在引发全行业对开源软件的安全问题进行探讨并寻求进步。
由于近期发生的事件,软件界对供应链攻击的实际风险有了更加深刻的了解。因为全部代码和依赖关系都公开可查验,开源软件在安全方面的风险相对较小。一般来讲确实如此,但前提是人们真的有在查验。由于依赖关系众多,要监控所有这些依赖并不切实际,而且很多开源包并没有得到很好的维护。
程序通常直接或间接地依赖于数千个软件包和库。举例来说,Kubernetes现在依赖大约1000个软件包。与闭源软件相比,开源软件使用了更多的依赖库,并且供应商来源更加广泛;因此需要信任相当多的不同实体。这使得理解产品如何使用开源软件以及如何发现相关漏洞是极其困难的。此外,没有办法可以保证构建出的程序与其源代码相匹配。
退一步来讲,虽然供应链攻击是一种风险,但绝大多数漏洞都是普通的、无意的--善意的开发者所犯的无心之过。此外,相较于亲自发现漏洞,恶意攻击者更倾向于利用已知漏洞,原因很简单:因为这会使得攻击更加容易。因此,我们必须集中精力做出根本性改变以解决大部分漏洞,唯有这样做才会使整个行业得以深入解决那些复杂问题(包括供应链攻击)。
几乎没有任何组织能够验证他们使用的所有程序包,更不用说这些包的更新了。就目前而言,跟踪这些软件包需要耗费大量的基础设施和显著的人力开销。我们在Google拥有这些资源,并不遗余力管理我们使用的开源包--包括为我们内部使用的所有开源包维护一个私有仓库--但想要跟踪全部的更新仍然困难重重。庞大的更新流令人望而生畏。所有解决方案的核心部分都是更高程度的自动化,这将是2021年及以后我们开源安全工作的关键主题。
因为这是一个需要行业合作的复杂问题,所以我们目的是围绕具体的目标展开对话。Google联合创立了OpenSSF并将其作为此次合作的重点。但是为了更进一步,我们需要整个行业参与进来,并就问题所在以及如何解决问题达成共识。作为开始,我们提出了解决此问题的一种方法,以及一系列具体的目标,希望这些目标可以加速产生整个行业的解决方案。
我们建议将这一挑战定义为三个基本独立的问题领域,每个领域都有具体的目标。
1. 知悉软件中的漏洞
2. 预防新漏洞的产生
3. 修复或消除漏洞
另一个相关但独立的问题是提高开发过程的安全性,这对保障供应链的安全至关重要。我们将在第四节"关键软件的预防措施"中概述该问题所面临的挑战并提出目标。
由于各种原因,知悉已有漏洞远比预期来的更困难。尽管存在漏洞报告机制,但很难确认软件的特定版本是否受到漏洞影响。
首先,从所有可用的数据来源中准确获取漏洞元数据至关重要。例如,知悉哪个版本引入了漏洞,将有助于判断某个软件是否受影响;而知悉何时漏洞被修复,就能准确及时地打补丁(并减少潜在的漏洞利用窗口期)。理想情况下,这种分流工作流程应当自动化完成。
其次,大多数漏洞都存在于依赖库中,而不是在你直接编写或控制的代码中。因此,即使你的代码没有改动,漏洞也会不断地出现,此消彼长。
2.2 目标:建立漏洞数据库的标准架构
需要建立基础设施和行业标准来跟踪和维护开源漏洞、了解漏洞产生的后果并管理缓解措施。标准的漏洞架构将允许通用工具跨多个漏洞数据库工作并简化跟踪任务,特别是当漏洞涉及多种语言或子系统时。
2.3 目标:准确跟踪依赖关系
我们需要更好的工具来快速了解哪些软件受到新发现漏洞的影响,而依赖树的巨大规模和动态特性使这一问题变得更加棘手。由于只有通过安装程序才能解析出软件的版本,因此如果不实际进行安装操作,当前也很难有办法准确地预测出被调用的软件版本。
理想的做法是预防漏洞的产生,尽管测试和分析工具有所帮助,但预防始终是一个难题。在这里,我们着重讨论两个特定方面。
1.决定采用新的依赖库时需要理解风险。
2.改进安全关键性软件(security-critical software)的开发流程。
3.1 目标:理解新依赖库的风险
第一个方面主要是:当你决定使用一个软件包的时候,需要了解它的漏洞。接受一个新的依赖库会带来固有风险,因此需要做出明智的决定。一旦使用了依赖库,随着时间的推移通常会变得越来越难以移除。了解漏洞是一个不错的开始,但我们还可以更进一步。
理想情况下,如果没有显式更新的情况,依赖库的版本应该是稳定的,但具体的行为因打包系统而异。Go Modules和NuGet这两个打包系统都以稳定为目标,而不追求快速升级,默认情况下它们都只在需求项更新时才会安装升级程序;依赖关系可能有误,但它们只在显式更新时才会去修改。
许多漏洞是由于在软件开发过程中没有遵守安全最佳实践而产生的。所有的贡献者是否都使用了双因素认证(2FA)?项目是否设置了持续集成并运行测试?是否集成了模糊测试?这些类型的安全检查将帮助使用者了解新依赖库带来的风险。对于"得分"较低的软件包需要仔细审查,并制定风险缓解计划。
OpenSSF最近公布的“安全记分卡”(Security Scorecards)项目尝试以全自动的方式生成这些数据。使用记分卡还可以帮助抵御猖獗的名称仿冒(Typosquatting)攻击(恶意软件包采用与流行软件包相似的名称),因为仿冒的恶意软件包得分会很低,并且无法通过很多安全检查。
改进关键软件的开发流程不仅仅与漏洞预防相关,我们将在后续章节中进一步讨论。
传统意义上的修复漏洞问题超出了我们的讨论范围,但对于管理软件依赖库中的漏洞这一具体问题,需要我们做的还有很多。虽然现在没什么帮助,但随着我们提高准确度,有必要对新的流程和工具进行投资。
当然,一种选择是直接修复漏洞。如果你能通过向后兼容的方式完成修复,那么该修复程序就可以为大家所用。但面临的挑战是,你不太可能拥有关于这个漏洞的专业知识,也没有办法直接进行修改。修复漏洞的前提是软件维护者意识到问题,并且公开漏洞相关的知识和资源。
相反,如果你只是简单移除包含漏洞的依赖库,那么对于你和那些导入或使用你软件的人来说漏洞被修复了,但对其他人则没有。这是一个可由你直接控制的变更。
上述场景描述的是你的软件与漏洞分别位于依赖关系链的两端,但在实践中可能还会存在许多其他关联软件包。大家普遍寄希望于依赖链上的某个人来修复它。不幸的是,仅修复一个环节是远远不够的。只有你和漏洞之间依赖链中的每一个环节都更新完毕后,你的软件才算被修复。每一个环节都必须引用其依赖链以下软件包的已修复版本来清除漏洞。因此,更新需要自下而上地进行,除非你能完全消除依赖关系(这或许需要类似的英雄胆识,而且几乎不可能发生——但在可能的情况下,这其实是最好的解决方案)。
时至今日,我们对这一过程仍缺乏清晰认知:别人已经取得了哪些进展?应该在哪个层面上应用哪个升级程序?这个过程又卡在哪里?谁来负责修复漏洞本身?谁来负责传播修复程序?
最终而言,你的依赖库漏洞将被修复,你可以在本地升级到新版本。但重要的是搞清楚这件事何时会发生,因为它可以快速减少漏洞的暴露风险。此外,我们还需要一个漏洞发现通知系统;通常来说,新漏洞往往意味着新发现了潜伏的问题,即使实际代码并未改动(比如Unix实用程序sudo漏洞已有10年的历史)。对于大型项目来说,大多数这样的问题出现于间接依赖库中。如今,我们还缺乏做好通知系统所需的准确度,因此在我们提高漏洞准确度和改进元数据(如上文所述)的同时,我们也应该推动通知系统的进步。
目前为止,我们仅描述了一种简单情形:一连串向后兼容的升级,这意味着除了漏洞被消除之外,行为没有区别。
但实际上,升级往往并不向后兼容,或者被版本限制要求所阻碍。这些问题意味着更新依赖树深处的软件包必然会在依赖链上层引起一些混乱,或者至少会导致需求项更新。这种情况通常出现在最新版本(比如说1.3版)有了更新程序,但你的软件或相关的包需要引用1.2版时。这种情况司空见惯,成为一项重大挑战,由于很难让开源项目的所有者更新相关的包,因此变得更加艰巨。此外,如果你在成百上千处使用了某个软件包(这对于大型企业来说并不疯狂),那么你可能需要经历成百上千次更新过程。
修复旧版本中的漏洞同样重要,尤其是那些使用频繁的版本。这种修复方式常见于那些拥有长期支持的软件中,但在理想情况下,所有广泛使用的版本都应该被修复,特别是包含安全风险的版本。
自动化方法可能会有所帮助:给定某个版本的修复代码,或许我们可以为其他版本自动生成良好的备选修复代码。目前这个过程有时是手工完成的,但如果此过程被大大简化,我们就能修复更多的版本,减少依赖链更高层的工作。
综上所述,我们需要更便捷且更及时地修复漏洞,尤其是依赖库中的漏洞。不仅是最新版本,广泛使用的版本也需要获得更多修复的机会,因为最新版本通常包含有其他变更,所以很难被采用。
最后,在"修复"方面还有很多其他选择,包括各种缓解措施,比如避免调用特定方法,或者通过沙箱或访问控制来限制风险。这些都是重要且可行的备选方案,理应得到更多的讨论和支持。
上述框架广泛适用于各种漏洞,无论是恶意攻击者蓄意造成的,或者仅仅是无心之过。虽然前面建议的目标涵盖了大多数漏洞,但仅靠这些并不足以防止恶意行为。为了对预防恶意攻击者(包括供应链攻击)做出实质性改变,我们需要改进开发流程。
这是一项艰巨的任务,并且目前对于大多数的开源项目来说都不切实际。开源之美某种意义上来说是由于缺少流程的约束,因此吸引了广泛的贡献者。然而,这种灵活性会阻碍安全方面的考虑。我们需要贡献者,但我们不能寄希望于每个人都同样关注安全问题。相反,我们必须确定关键软件包并加以保护。尽管可能会增加开发人员之间的摩擦,但这些关键包必须遵循一系列更严格的开发标准。
确定广泛被依赖的"关键"软件包至关重要,若失陷会危及关键基础设施或用户隐私。因此这些软件包必须遵循更高的标准,我们在下文中概述了其中一些标准。
如何定义"关键"的标准并不明确,随着时间的推移,定义的范围可能还会扩大。除了显而易见的软件,如OpenSSL或密钥加密库,还有一些被广泛使用的软件包,其覆盖面之广使它们值得受到保护。Google启动了关键性得分项目(Criticality Score project),与社区一起对该问题集思广益,同时与哈佛大学合作开展开源普查(Open Source Census)工作。
我们在Google中遵循的一个原则是,变更不应该是单方面的--也就是说,每一个变更都至少涉及一个作者和一个审核者/批准者。目标是限制对手可以凭借一己之力做的事情--我们需要确保有人真正在检查这些变更。对于开源项目来说,要做好这一点其实比在一家公司内部要难得多,因为公司内部拥有严格的身份认证,并且会执行代码审查和其他检查。
避免单方面变更可以分为两个子目标。
1.目标:要求对关键软件进行代码审查。
代码审查不仅是改进代码的好方法,还能确保除了作者之外,至少有一个人在检查每一项变更。代码审查是Google内部所有变更的标准做法。
2.目标:对关键软件的变更需要得到两个独立方的批准。
为了真正实现"有人在检查"的目标,我们需要审查者独立于代码贡献者。而对于重大变更,我们可能需要不止一个独立审查。当然,我们需要理清到底什么才算是"独立"审查,但对大多数行业内的审查而言,独立性思想都至关重要。
独立性的概念通常意味着你了解参与者——匿名参与者不能被认为是独立的或值得信赖的。时至今日,我们基本上都使用化名:同一个人反复使用同一个身份,从而积攒声誉,但我们并不了解这个人是否可信。这就引出了一系列的子目标。
1.目标:对于关键软件,所有者和维护者不能匿名。
攻击者都喜欢匿名。在过去的供应链攻击中,攻击者利用匿名性,通过软件包社区努力成为维护者,而没有人意识到这个"新的维护者"具有恶意企图(入侵代码最终被注入上游)。为了减轻这种风险,我们认为关键软件的所有者和维护者一定不能匿名。
可以想见,与所有者和维护者不同,贡献者可以匿名,但前提是他们的代码已经通过了可信人士的多次审查。
也可以想见,我们可以拥有"已验证"的身份,也就是说:有一个可信实体知悉他的真实身份,但出于隐私原因,公众并不知道。这将有助于有关独立性的决策以及对非法行为进行起诉。
2.目标:为关键软件的贡献者提供严格的身份认证。
恶意攻击者会寻找容易的攻击载体,所以网络钓鱼攻击和其他形式的凭证盗窃行为很常见。一个显而易见的改进措施是要求使用双因子认证,尤其是对于所有者和维护者。
3.目标:身份的联合模型
为了继续保持开源的包容性,我们需要能够信任各式各样的身份,但是可验证的完整性仍不可或缺。这意味着身份的联合模型,也许类似于我们今天支持联合SSL证书的方式——多个团体可以生成有效的证书,但必须具备严格的审计和相互监督。
OpenSSF的数字身份认证工作组已经开始对该话题进行讨论。
为涵盖风险的变化,我们应当扩大通知的范围。最明显的是所有权变化,这可能是新攻击的前奏(如最近的NPM事件流(NPM event-stream)失陷)。其他例子包括发现凭证被盗、串通或其他恶意行为者的行为。
通常使用安全散列来检测构件(artifact)是否完好无损,使用数字签名来证明真实性。增加"透明度"意味着这些证明会被公开记录,从而将所有意图文档化。反过来,即使用户没有感知,外部各方也可以监控日志中是否存在伪造版本。更进一步来说,当凭证被盗时,我们可以了解到这些凭证被用来签署了哪些构件,并可以设法删除它们。这种透明性,包括持久的公共日志和第三方监控,已被成功应用于SSL证书,我们也为包管理器提出了一种类似方法。知悉你使用了正确的软件包或二进制文件,类似于知悉你正在访问一个网站的真实版本一样。
1984年,肯-汤普森著名的图灵奖演讲证明了仅靠真实可信的源代码还远远不够,最近的事件表明构建过程攻击也是一种真正的威胁。如何确认你的构建系统是可信的?它的所有组件都必须可信,并通过一个持续的建立信任的过程来验证。
可重复构建(Reproducible builds)会有所帮助(构建具有确定的结果,由此我们可以验证我们的构建是正确的),但由于临时的数据(如时间戳)最终会出现在构建的发行版中,因此这很难实现。而安全可重复的构建需要验证工具,而验证工具又必须经过可验证和可重复的构建,以此类推。我们必须打造一个可信工具和构建产品的网络。
对构件和工具的信任都可以通过"委托"(上述透明过程的变体,被称为二进制授权)来建立。在Google内部,构建系统会对所有构件进行签名,并生成与源代码相关联的清单。对于开源项目来说,一个或多个受信任的代理可以将构建作为一项服务来运行,对构件签名以证明他们对其完整性负责。这种生态系统理应存在,大部分情况下只需要大家都意识到这一点,并按照证明文书格式签署一些协议,我们就可以安全地自动化上述流程。
本节所述的措施非常适合于一般性的软件,目前在Google内部已广泛使用,但是对于开源项目来说,它们的工作量要大得多。我们期望通过专注于少数关键的软件,至少在关键软件上实现这些目标。随着工具和自动化程度的提高,这些目标将更容易被广泛采用。
开源的本质要求我们通过共识和协作来解决问题。对于漏洞等复杂主题,这意味着要围绕关键问题进行集中讨论。我们为这种讨论提出了一种框架方法,并定义了一系列目标,我们希望这些目标能够加速整个行业的讨论并得出最终的解决方案。第一组目标广泛适用于各种漏洞,其实质是关于实现自动化,减少风险和辛劳。
原文链接: