我们如何修复600多个Ruby应用程序中的依赖混淆漏洞
多年来,Shopify有了长足的发展,我们的成功使我们成为了对恶意行为者有吸引力的目标。我们非常重视商户的安全,所以我们有充分的理由不断提高Shopify的安全性。
我将分享Ruby公约团队(专注于创建公约以使Ruby服务可持续发展)如何使用迭代方法来大规模解决复杂问题,同时应对不断变化的环境。特别是,我们是如何解决600多个Ruby应用中的依赖混淆漏洞,开发出允许我们轻松进行大规模迁移的工具,并使Ruby社区变得更加安全。
了解依赖性混淆问题
Shopify实施了一个错误赏金计划,我们付钱给人们来发现我们平台上的漏洞,并了解我们需要改进的地方。其中一份报告显示,我们有一个依赖关系混乱的漏洞,可以让攻击者进入我们的本地、持续集成/持续部署(CI/CD)和生产环境。
该漏洞利用软件包来源的模糊性来安装恶意的依赖关系。如果一个外部包在与Shopify内部包相同的名称下创建了更高的版本号,那么外部依赖将被解决,而不是内部依赖。
在Ruby中,开发者使用Bundler来管理他们的依赖关系,并使他们的环境具有可重复性。捆绑器解决了依赖关系,这样你就可以为每个宝石使用正确的版本和来源。Bundler团队通过引入一种新的Gemfile.lock 文件格式来解决这个问题,这种文件格式由新的安装或更新创建。新的格式将每个 gem 分配给一个明确的源。
然而,在当时,新格式要求你升级。这意味着Bundler更新了lockfile中的所有依赖关系,这就需要对每一次更新进行审查,并测试应用程序的行为回归。
识别影响
我们不知道有多少应用程序容易受到依赖关系混乱漏洞的影响,这使得我们很难评估该问题的影响。我们的第一步是消除歧义,以便我们能更好地理解这个问题。
消除未知因素不需要很花哨,有一些了解总比没有了解要好。在我们的案例中,我们在CI系统中定义了一个cron job,从所有软件库中获取Bundler版本信息到我们的数据湖。结果发现,大约有600个Ruby应用程序容易受到依赖性混乱漏洞的影响。
有了这些数据,我们还可以创建一个未完成迁移的指标,并衡量解决我们问题的进展。这也是一种将解决方案从目标中分离出来的好方法,这样可以减少约束。
通过实验改变假设
作为开发者,我们的解决方案必须考虑到相当多的约束。在迭代开发软件时,我们尝试改变其中的一些约束条件,并迅速重新评估我们的解决方案。尽快做出这些改变,可以浮现出未知数,增加项目成功的可能性。
在我们的案例中,有超过600个存储库需要迁移,这意味着手动迁移每个应用程序将太耗时。要求团队自己来做会很乏味,而且容易出错,因为Gemfile.lock 文件不能自动更新,同时保持当前的gem版本。在这种情况下,开发人员将需要修改锁文件,将版本更新还原,以防止引入退步。
如果我们能够在不更新依赖关系的情况下将Gemfile.lock ,这将使我们能够在Shopify的所有Ruby应用程序中自动推出这一升级。我们将只依靠应用程序的所有者来部署这些变化。
我们尝试建立一个Bundler插件(一个扩展了Bundler功能的宝石)来自动升级。它将Gemfile.lock 文件更新为新的格式,而无需更新依赖关系。该插件归结为:
- 初始化一个给定的
Gemfile.lock文件的规范,该文件包含宝石的信息,如名称、版本和远程。 - 将
Gemfile.lock文件更新为新的lockfile格式,在此过程中更新所有宝石。我们通过只允许补丁版本的更新来减少更新。 - 用旧的
Gemfile.lock文件中的宝石版本替换更新后的Gemfile.lock文件中的版本。
这种方法并不是一个完美的解决方案,但它在运行Bundler迁移时效果很好。它使我们能够进入下一个问题领域,即迁移大量的应用程序。
规模化地运行迁移
运行大规模迁移的最大挑战之一是处理边缘案例。与其事先探索迁移如何出错,不如迁移少量的应用并发现实际问题,这样做更有效。另一个好处是,我们可以识别并迁移有问题的应用程序子集,这些问题有已知的解决方案,同时解决了边缘案例。这种方法使我们能够不断地实现我们的目标,并使我们每天都处于一个更好的位置。
我们的Bundler插件在没有依赖性更新的情况下迁移了锁文件,然后我们就可以开始迁移应用程序了。我们开始在少数几个不面向商家的应用程序上运行该插件。这很顺利,我们决定在一个更大的批次上运行它,用于非关键的存储库。然而,我们注意到在大批量的迁移中,由于构建设置、Ruby版本和其他配置的不一致而产生的问题。
我们的一些工具不支持最新的Bundler版本,我们不得不与我们的部署、CI和本地环境团队合作来更新它们。我们的合作在以下情况下特别富有成效:
- 首先调查了问题
- 尝试解决这个问题
- 与团队分享背景。
大多数人都想提供帮助,让他们轻松一点对大家都有好处。
我们的一些Docker镜像是用Heroku的Ruby buildpack构建的,不支持所需的Bundler版本。这种情况导致一定比例的应用程序无法迁移。为了解决这个问题,我们与Heroku Buildpack团队合作,采用最新的Bundler版本。他们随bundler的更新发布了一个新的版本,使其在Ruby社区广泛使用。
另一个关键因素是提高项目所有者的意识,并设定一个废除旧版Bundler的最后期限。直面业主,沟通变化的影响,使团队能够优先考虑并与我们合作,更新他们的项目。
Bundler迁移插件在本地运行,但出现了可扩展性问题。要管理不同的Ruby版本,使其并行化,并解决故障,变得太复杂了。我们没有把时间浪费在建立一个一开始就能解决所有问题的解决方案上,而是把迁移插件用到了它的极限,调查了问题所在,并进行了改进。
作为对扩展问题的回应,我们在CI系统的基础上建立了一个命令行界面(CLI)工具,为版本库建立正确的环境,在上面运行命令,并根据所做的修改打开一个拉动请求(PR)。每个版本库有一个环境,效果很好,因为我们不再遇到错误的配置问题了。使用我们的CI系统也使我们能够并行执行,这反过来又加快了进程。此外,迁移失败也更容易恢复和跟踪。
预防未来的问题
迭代解决问题的一部分意味着要关注当前的问题而不是未来的问题。然而,这并不意味着完全忽视未来的问题。重要的是要区分关键问题和那些可以在以后解决的问题。
一个例子是防止一个Gemfile.lock 文件倒退到以前的格式,这将使我们受到攻击。我们意识到回归的可能性,但我们也知道我们可以建立工具来解决这个问题。我们没有在前期投入时间来解决这个问题,而是决定等待,一旦我们迁移了大部分应用程序,就开始着手解决这个问题。这种方法也使我们能够衡量问题的严重性,而不是浪费资源来解决假设性问题。
在我们的迁移过程中,我们遇到了一些回归的问题,有点担心。我们手动调查了每个问题,看看是否有更大的问题存在。由于我们没有发现任何暗示更深层次问题的东西,我们继续进行监测,知道如果我们遇到更多的回归,我们有更多的信息来改变方向和面对新的现实。
我们调查了lockfile的回归问题,并与Bundler团队分享了我们所学到的东西。他们增强了该工具,以防止这些情况在未来发生。我们不需要实施特殊的工具来防止回归(这为我们节省了大量的工作和时间)。我们只需要确保所有的应用程序都使用正确的Bundler版本。
我们的大多数应用程序都被迁移到了不需要防止回归的Bundler版本,因为我们错开了迁移的时间,使之不断进步。由于我们对迁移工具进行了实战测试,并解决了大多数配置问题,这使得我们能够在不到一天的时间内将所有的应用程序迁移到最新的Bundler版本。
与其等待完美的解决方案,不如进行反复的修改,改进我们的工具,使我们把过去很难的修改变得简单。这使部署的风险降低了。
为了防止恶意宝石的安装,我们对本地环境工具进行了修改,以确保它总是默认为推荐的Bundler版本。这确保了单个开发人员的机器不容易运行来自依赖性混乱漏洞的恶意代码。我们也开始在CI遇到过时的Bundler版本时失败,确保任何可能引入依赖性混淆漏洞的代码变化不会被合并。由于我们的大多数其他自动化进程都需要CI来执行,我们依靠CI来捕捉有漏洞的Bundler版本。
与社区分享我们所做的事情
在Shopify,我们热爱开源,我们也喜欢回馈社区。在做出贡献时,分享目的和解决方案是相当有价值的。它导致了有洞察力的对话,从而产生了更好的解决方案。通常,贡献并不只是公关。提供调查工作的背景,让别人注意到问题,或者测试另一个贡献者的原型,都是同样有价值的。
我们的插件对我们来说效果很好,所以我们在Bundler中创建了一个提案,为Ruby社区解决这个问题。这些改变将允许Bundler更新Gemfile.lock 文件,而不需要在这个过程中升级宝石。我们的提议没有被采纳,但却引发了一场对话,导致了另一种方法的出现,并在Bundler 2.2.21中得到了体现。我们帮助在我们的应用程序上测试他们的方法,以确保我们尽可能多地捕捉到边缘案例,以帮助减少社区的潜在负担。
我们也遇到了一些问题,使用不安全的Bundler版本的开发者可能会意外地恢复到旧的锁文件格式。问题是,最新的Bundler版本(当时)仍然在`bundle install`上解决旧的Gemfile.lock 文件,这使得倒退到旧的格式非常简单。我们创建了一个原型来防止这种情况的发生,这引发了与Bundler维护者的另一次对话,并引起了他们对这个问题的注意。他们发布了2.2.22版本的Bundler,以防止倒退,并使社区的每个人都更安全。
我们着手修复Shopify的每个Ruby项目中的依赖性混淆漏洞,并且成功了。如果我们没有遵循一个迭代的方法,让我们在考虑到变化的情况下取得稳定的进展,这是不可能的。我们开发了允许我们进行大规模迁移的工具,这在其他用途上也很方便。我们还在我们的Ruby项目上汇总了Bundler版本数据,以跟踪采用情况,使未来的决策更容易。最后,我们与Bundler团队紧密合作,改进基本功能,同时利用Shopify的规模来寻找边缘案例,修复错误,改进Bundler,并使其更好地为Ruby社区的每个人服务。