现代包装商的安全噩梦

2021-02-21 22:15:27

分发打包程序最重要的任务之一是确保交付给我们用户的软件没有安全漏洞。虽然通常将查找和修复易受攻击的代码视为上游的责任,但打包程序需要确保所有这些修复均尽快到达最终用户。借助中央软件包管理和动态链接,Linux发行版已非常完善了安全修补程序的部署。理想情况下,修复易受攻击的依赖关系就像通过发行版的自动更新系统修补单个共享库一样简单。

当然,仅当所涉及的软件包实际上遵循良好的安全惯例时,此方法才有效。多年来,许多Linux发行版(至少是Debian,Fedora和Gentoo)一直在与这些不良做法作斗争,并取得了一些成功。但是,今天时代已经改变。如今,每修复10个软件包,就会出现一个全新的生态系统,其核心问题是不良的安全做法。 Go,Rust和Python在某种程度上只是编程语言的一些示例,这些编程语言已将不良的安全实践集成到其存在的结构中,并以全新的方式重现了相同的旧问题。

捆绑依赖项的根本问题已经讨论了很多次。 Gentoo Wiki解释了为什么您不应该捆绑依赖项,并链接到有关它的更多材料。我想采用一种更广泛的方法,不仅讨论捆绑(或供应)依赖关系,还讨论两个密切相关的问题:静态链接和固定依赖关系。

简而言之,静态链接意味着将程序的依赖项直接嵌入到程序映像中。与动态链接(或动态加载)相比,该术语通常与动态链接(或动态加载)形成对比,动态链接将依赖库保留在单独的文件中,这些文件在程序启动(或运行时)时加载。

为什么静态链接不好?主要问题在于,由于它们已成为程序的组成部分,因此不能轻易用其他版本替换它们。如果发现其中一个库易受攻击,则必须将整个程序与新版本重新链接。这也意味着您需要拥有一个跟踪各个程序中使用的库版本的系统。

尽管您可能认为重建很多软件包只是源代码发行版的问题,但是您错了。尽管确实会严重影响源代码分发的用户,但由于他们的系统在很长时间内仍很脆弱,无法重建大量软件包,因此类似的问题也会影响二进制分发。毕竟,发行版需要重建所有受影响的程序,以便将修复程序完全交付给最终用户,这也需要一些延迟。

相比之下,发布共享库的新版本花费的时间要少得多,并且几乎可以立即修复所有受影响的程序(以重新启动它们的必要性为准)。

静态链接的极端情况是分发专有软件,该软件静态链接到其依赖项。这样做主要是为了确保可以在各种系统上轻松运行该软件,而无需用户手动安装其依赖项。但是,这种情况实际上是捆绑依赖项的一种形式,因此将在相应部分中进行讨论。

但是,静态链接在历史上也曾用于系统程序,即使它们的依赖库损坏了,这些程序也可以继续工作。

在现代程序包中,完全由于另一个原因使用了静态链接-因为它们不需要现代编程语言来具有稳定的ABI。 Go编译器不必担心发出与来自先前版本的代码二进制兼容的代码。通过要求您在每次升级编译器时都重建所有内容,从而解决了该问题。

为了遵循最佳实践,我们强烈建议不要在C及其派生类中进行静态链接。但是,对于诸如Go或Rust之类的将静态链接作为其设计核心的语言,并屡次公开声明它们不会切换到依赖关系的动态链接,我们不能做太多事情。

虽然静态链接不好,但至少它为自动更新(以及漏洞修复的传播)提供了一种合理清晰的方式,固定依赖项意味着需要安装程序依赖项的特定版本。尽管确切的结果取决于生态系统和固定依赖项的确切方法,但通常这意味着您的软件包中至少有一些用户将无法自动将依赖项更新为较新的版本。

乍一看似乎还不错。但是,这意味着,如果为依赖项发布了错误修复程序,或更重要的是,发布了漏洞修复程序,则除非您更新引脚并进行新发布,否则用户将无法获得它。然后,如果其他人固定了您的包裹,那么该固定点也将需要更新和释放。和链继续。更不用说如果某个软件包恰巧间接固定到同一依赖项的两个不同版本时会发生什么!

人们为什么要固定依赖关系?主要原因是他们不希望依赖更新突然破坏最终用户的软件包,也不希望其CI结果突然被第三方更改破坏。但是,所有这些还有另一个潜在的问题-既不关心上游部分的API稳定性,又不希望不必要地更新下游部分的工作代码(使用已弃用的API)。事实是,钉扎会使情况变得更糟,因为钉扎会掩盖所有问题,并积极鼓励人们针对特定版本的依赖而不是针对稳定的公共API开发代码。希律律在实践中。

依赖固定可能会产生非常极端的后果。除非您确保经常更新引脚,否则您可能有一天会发现自己必须突然采取措施-因为您依赖的是一个非常老的依赖关系版本,而现在已知该依赖关系很容易受到攻击,因此为了进行更新,突然不得不重写大量代码以跟随API的更改。从长远来看,这种方法根本无法扩展,保持工作正常进行所需的工作量呈指数增长。

我们努力解除依赖关系,并使用最新版本的测试包。但是,通常我们最终发现新版本的依赖项与所讨论的软件包不兼容。令人遗憾的是,上游企业经常忽略这些不兼容的报道,甚至对我们没有采取针锋相对的态度而积极地敌视我们。

现在,最糟糕的是-结合了上述所有问题,并且增加了更多的问题。捆绑(在Newspeak中通常称为供应商)意味着包括程序的依赖项。捆绑的确切结果取决于所使用的方法。

在开源软件中,捆绑通常是指要么将依赖项的源与程序一起包括在内,要么使构建系统自动获取它们,然后与程序一起进行构建。在封闭源代码软件中,通常意味着将程序静态链接到其依赖项,或者将依赖项库与程序一起包括在内。

基准问题与固定的依赖项相同—如果其中一个存在缺陷或脆弱,则用户需要等待新版本来更新捆绑的依赖项。在使用动态库的开源软件或封闭源软件中,打包程序至少有合理的机会来替换有问题的依赖项或将其完全拆开(即强制系统库)。在静态链接的封闭源代码软件中,通常甚至无法可靠地确定实际使用了哪些库,更不用说它们的确切版本了。您的发行版不再能够可靠地监视安全漏洞;信任转移到软件供应商。

但是,现代软件有时会更进一步-并且供应商修改了依赖性。恐怖了!现在,不仅打包程序需要工作以替换库,而且经常还必须实际找出与原始版本相比更改的内容,并对更改进行重新设置基础。在最坏的情况下,代码从上游断开连接,以至于程序作者不再能够正确更新供应商的依赖关系。

可悲的是,随着最近几天的快速发展,这种供应正在变得越来越普遍。原因是双重的。一方面,下游使用者发现与依赖项相比,分叉和修补依赖项要容易得多。另一方面,许多上游并不真正关心不影响其自身项目的错误和功能请求。即使分叉仅被视为一种权宜之计,但通常仍需要大量工作才能将更改推后推向上游并重新同步代码库。

我们强烈反对捆绑依赖关系。只要有可能,我们都会尝试将它们拆开—有时必须实际修补构建系统以重用系统库。但是,这是一项繁重的工作,而且由于自定义补丁程序(包括已被上游明确拒绝的那种补丁程序),通常甚至是不可能的。仅举几个例子-Mozilla产品依赖于SQLite 3补丁,这些补丁与该库的常规用法相冲突,Rust捆绑了庞大的LLVM分支,并明确拒绝支持使用正版LLVM库的发行版。

静态链接,依赖项固定和捆绑是三个不良做法,它们对消除生产系统中的漏洞所需的时间和精力产生了严重影响。它们可以在几分钟内替换易受攻击的库与必须花费大量的精力和时间来查找易受攻击的库的多个副本,修补和重建包括它们的所有软件在内,产生很大的不同。

主要的Linux发行版在很长一段时间内都针对这些做法制定了政策,并且一直在努力消除这些做法。但是,它越来越像西西弗斯式的任务。尽管我们已经能够成功解决许多问题,但全新的生态系统是建立在这些不良做法之上的,而且上游似乎根本不关心解决这些问题。

Go和Rust等新的编程语言完全依靠静态链接,因此我们无能为力。无需打包依赖项并让程序使用最新版本,我们只需获取上游固定的版本,并从中获取大笔费用。尽管上游公司吹嘘他们如何神奇地解决了您可能想到的所有安全问题(完全忽略了与内存相关的其他安全问题),但我们只是希望,当遇到共同的固定依赖关系时,我们不会突然陷入困境许多软件包中的一个很脆弱。