首页 >> 大全

做好依赖管理的十五条准则(下)

2023-10-07 大全 26 作者:考证青年

但这也不是绝对,有些代码确实算是「完成」了。

譬如 NPM 的包--,很早就有了,但基本不需要再修改了。

5、被使用的次数

依赖包管理器平台通常可提供使用情况的统计数据,或者你可以去搜索引擎搜索来判断是否很多人用此包。很多人用的话,至少说明这份代码能满足这些人的使用,并通常能快速发现新问题。

广泛的使用量,其实也为持续维护提供保障,因为若维护者不维护了,还会有其他有兴趣的人去接手维护。

举例来说,像PCRE、Boost、JUnit这些库被极为广泛地使用着。在你碰到问题前,很可能别人已经碰到,且已经修复了。

6、安全性

举例来说,2006 年 代码搜索系统,用了grep而非开源的代码。当时流行的PCRE正则表达式库似乎是一个不错的选择。

但 的安全团队说,PCRE有一系列的问题包括缓冲区溢出(特别是其解释器中)。

在 NVD 中也能找到关于PCRE的相关问题。

但是,代码搜索系统并没放弃使用PCRE,而是更谨慎的进行测试和问题隔离。

7、查看许可证

上,有一部分代码库没有明确标明许可证。

然而,此时此刻允许,并不表示你的项目或公司能一直可以使用这些依赖。

譬如, 就不允许使用基于类似这几种许可证的代码:AGPL(容易有法律风险)、WTFPL(过于模糊)。

8、检查一下依赖包的依赖

间接依赖中的问题与直接依赖中的问题都是一样的。依赖包管理器平台可以列出包的所有依赖情况,所以理想情况下,你应逐一进行检查。间接的依赖也可能会导致风险,一个包自身的依赖需要更多的检查工作。

很多开发者从来不检查代码的间接依赖,也不知道间接依赖了什么。

例如,2016 年 03 月,NPM 社区发现许多广泛使用的项目(如 Babel、Ember、React)都间接依赖了一个很小的库letf-pad(1 个只有 8 行的函数)。而left-pad的作者从 NPM 上删除了这个包,这导致很多 Node.js 用户的项目构建失败,甚至连left-pad自己也不例外。

例如,NPM 上总共约 750,000 个包,其中 30% 直接或间接依赖于--。根据 (图灵奖得主) 对分布式系统的观测,依赖包管理器平台可以轻松让一个包失效,从而导致你的包莫名变得不能用,例如无法编译构建打包。

9、通过自己的测试来检查

如前所述,检查工作应包含运行该依赖包自带的测试代码。但是,即使包自带的自动化测试可以通过,你也应该在使用这个包之前,继续为你所使用的相应功能写自动化测试用例。

这些测试用例应是简短的、独立的程序,以便于日后容易理解你的 API 对应的测试。

如果你现在还没写测试用例,就立即写吧,回头是岸~

花些额外精力写测试代码是很值得的,因为即使将来依赖的包的版本升级了,你的功能也会有保障。若你发现一个 bug,并且你自己有可能修复它,那么你可以重新执行一次你项目的测试用例,以确定不会影响到其他功能。

运行基本的检测来发现可能存在的问题。

以 代码搜索系统为例,根据经验,PCRE有时需要很长时间执行某些特殊的正则表达式搜索。最初的计划是将搜索分为「简单」和「复杂」两种正则表达式,并跑在互相独立的线程池中。

第一阶段的测试中,谷歌跑了一些对比和一些其他grep实现的基准测试。当在一个测试用例中发现比最快的grep实现慢了近 70 倍时,谷歌开始重新考虑是否还该继续用PCRE了。

尽管最后谷歌还是放弃了PCRE,但时至今天这个测试用例依然保存在我们的代码库中。

引用后的使用准则

(共 6项)

1、对外部依赖进行封装

不同包的情况,你可能会因为下面因素而考虑是否继续使用它:

基于上述原因,你需要额外工作来方便迁移到新的依赖。

如果你项目代码中很多地方用到了这个依赖包,那当迁移到一个用于替代它的新依赖包时,你需要修改的地方会很多。

更麻烦的是,若你提供给外面的 API 包含了依赖包的 API,那当你迁移到新的依赖时,则使用了你 API 的代码都得修改,而那些调用了你 API 的代码,你可能是无法控制的。

为了避免依赖蔓延而导致的替换成本,你应该

理想情况下,这样可以让你仅需要修改封装器,就能适配不同的依赖包。

迁移到新的依赖后,记得也修改所以对应的测试用例,以后还可以继续换依赖。

在 代码搜索系统中,开发了一个抽象的类,类中定义了代码搜索接口,支持任意正则表达式引擎。

_做好依赖管理的十五条准则(下)_做好依赖管理的十五条准则(下)

然后为PCRE写了一个轻量的封装器来实现该接口。这种间接的方式可以更简单的测试不同的库,并可以避免去了解 PCRE 的内部细节。当然,这也使得我们更容易在不同的库之间切换。

2、对依赖进行隔离

在程序运行的时候,隔离依赖可避免因为这个依赖有 bug 而导致的崩溃。

例如, 浏览器允许用户添加依赖(即扩展)。

2008 年 发布,引入了一个很关键的功能:将每个扩展插件隔离在运行在独立系统进程的沙箱中。因此,有 bug 的扩展插件并不能访问到浏览器的全部内存,并可通过系统调用停掉扩展进程。

在 代码搜索系统中,一直将PCRE解释器运行在一个类似的沙箱中,直到弃用它。现在,可以选择基于轻量级管理程序的沙箱(如)。将依赖隔离起来可减少很多风险。

即使有示例,也有现成的选项,在运行时隔离代码依然比较困难,而且的确很少人做到了。

真正的隔离需要一个完全内存安全的语言,不需要转换为无类型的代码。这不仅对 C、C++ 等完全不安全的语言有挑战,也对提供受限制的不安全操作的语言提出挑战,如含 JNI 的 Java、和含有「不安全」功能的 Go、Rust、Swift。

即使在内存安全的语言中如 ,其代码通常也可访问远远超过它所需要的地方。

NPM 包event-为 事件提供流式 API,在其 2018 年 11 月的最新版本中发现了两个半月前添加了混淆过的恶意代码。这些恶意代码从名为Copay的移动 App 中收集了大量比特币。这些代码访问了与事件流完全无关的系统资源。针对此类问题,限制依赖的访问权限是防御措施之一。

3、如果无法隔离,最好避开

如果某个依赖包的风险太大,而且你也找不到隔离的办法,那最好的办法就是完全不用它。或至少不要使用有问题的那部分。

例如,随着谷歌对PCRE风险、成本的更深入了解,在其代码搜索系统中,使用方式也不断发生了变化。这些变化以下面的顺序发生:

直接使用PCRE

在沙箱中使用PCRE的解释器

写一个新的正则表达式解释器,但依然使用PCRE的执行引擎

写一个新的解释器,并连接到一个不同的,且更高效的开源执行引擎

我们重写执行引擎。

在重写以后,谷歌对它就没有任何依赖了,我们后来将其开源并命名为RE2

如果你只需依赖包中的一小部分功能,最简单的方法可能是直接复制你所需的那部分下来(当然,也要保留适当的版权和其他法律声明)。这时你需要自行修复 bug、维护等等,但好处是你可以与较大的风险隔离开来。

Go 开发者社区有一句谚语:“小的复制比小的依赖更好”。

4、依赖的更新策略

关于软件的传统观点是「如果没有问题,就不要动它」。因为,依赖包的升级可能会引入新的 bug ,而且升级也不像增加新功能那样带来多少好处,何苦要冒这个险呢?

但上述观点忽略了两种成本,它们分别是:

升级的成本。最终还是要升级,因此而带来的成本(如 Log4j 的漏洞)。在软件中,修改代码的难度并不是线性递增的。10 次小改动比 1 次大改动更少工作量,也更不容易出问题。

问题跟踪的成本。很难发现之前新版本中已被修复过的 bug。尤其是在与安全相关的环境中,外面已知的漏洞都会被人利用,你每天都有可能被攻击者入侵。

例如,(一家跨国征信公司)的高管在 2017 年国会证词中的陈述。

当年 3 月 7 日, 爆出一个新漏洞,并发布了修复版本。

3 月 8 日, 收到 US-CERT 应更新 的通知。

3 月 9 日、3 月 15 日, 分别进行了代码和网络扫描,并未发现有涉及漏洞的外网服务器。

5 月 13 日,攻击者发现了( 的安全团队未发现)依然有存在漏洞的服务器。攻击者利用 漏洞入侵了 的网络,并在接下来的两个月内盗取了约 1.48亿人的详细个人信息和财务信息。

公司最终在 7 月 29 日发现被入侵,

并在 9 月 4 日进行了公开说明。

同年 9 月, 的 CEO、CIO、CSO 已全部辞职,并且国会开始介入调查。

的经验告诉我们,虽然依赖包管理器平台知道构建代码时所使用的版本,但你还是需要另外的工作来跟踪线上部署过程的信息。

对于 Go 语言,我们正尝试在每个二进制文件中自动包含版本清单,以便在部署过程中可找到所需升级的依赖项。Go 还可以在运行时提供这些信息,这样服务器就可以通过查询依赖库中的已知 bug ,并在需要升级时自动向监控服务发送报告。

及时升级依赖固然重要,但升级就意味着向项目添加新代码,这时,我们仍旧需要去评估新版本依赖中可能带来的风险。

至少,你应稍看一下版本间的代码差异,或看一下版本发布说明,来确定关键位置的升级代码。

如果实在有太多的差异代码,导致难以通过差异信息来发现问题,那么,你应把这个问题也作为一个升级风险来对待。

并且,版本升级不应完全自动化。

在升级前,你应预先验证新版本是否能在你的环境运行。

安全相关的关键升级窗口期特别短。

如果你的升级过程包括运行以前所写的整合测试用例与合规测试用例,那你已有能力在上线前预先发现问题了。在这种情况下,你升级越快,风险越低。

在 公司的入侵事件发生后,法院的安全团队发现证据表明攻击者(可能是不同的攻击者)在 漏洞爆出后仅第 3 天(即 3 月 10 日)就已经入侵了 的服务器,但他们当时只运行了一个命令。

5、对依赖的监控

即使你已经做到上述所说的,工作仍旧没有完成。

目前大部分依赖包管理器可以轻松做到(甚至自动地记录)某个版本代码的加密哈希值。在其他电脑或测试环境中重新下载依赖包后,可验证哈希值是否一致。

这样可以保证你的代码构建是基于你曾检查过、测试过的、完全相同的依赖代码以从而避免类似event-那样的攻击(偷偷地在已发布的版本 3.3.5 中插入了恶意代码,因为没有做哈希校验)。若有哈希校验,则攻击者必须创建一个新的 3.3.6 版本,并等人们升级(且是没留意是否有修改的前提下)才能攻击成功。

另外,还需注意是否有新的间接依赖被加入进来了。版本升级也很容易在你升级目前的依赖时引入新的间接依赖。

在event-这个案例中,恶意代码被隐藏在一个不同的包中-,而event-发布新版本时就将这个包作为新依赖引入进来了。

这些间接依赖所造成的影响也会体现在你项目构建后的包的尺寸上。

在 的 (一门 JIT 日志处理语言)中,作者发现:

在不同时候,其主解释器二进制文件不仅包含 的 JIT 信息,还包含从未使用过的 、、 的解释器。

每次细查发现,其原因是 所依赖的某些库声明了一些从未使用的其他依赖,再加上 的构建系统会自动处理新的依赖关系,从而导致上述结果。

这类错误正是 Go 语言为什么会将未使用过的导入当成是编译时错误的原因。

6、什么时机用来检查是否继续使用原有的依赖

当然是升级版本时。

定期重新检查依赖是否有变化也很重要。

重新查看每个依赖项的安全相关历史记录也很重要。

例如, 在 2016、2017、2018 均披露过不同的严重远程代码执行漏洞。

所以,即使你的服务器都已升级到其最新版本,但有着这样的安全历史记录,你应再三思考是否应继续使用它。

3 条小建议

软件复用的时代已经来临,

我们不能低估其带来的好处:软件复用为开发者带来了巨大的便利。

尽管它的好处固然不可否认,但我们在这股转变的洪流中,还没来得及认真去思考随之而来的风险。

今时不同往日,我们拥有太多依赖,已经不能再像二三十年前那样「过于信任依赖」了。

说实在的,对依赖项进行严格的验证、测试会有很大的工作量,并且大部分团队做不到这一点。

我甚至怀疑是否真的有开发者做到了对每个新引入的依赖都做了上述工作。

大部分情况下的决策完全就是「先用着再看」,

而且,再往前一步就会增加很多工作量。

但 Copay 和 公司被入侵的案例已经敲响了警钟:

我们现在这种使用依赖的方式真的存在严重问题。我们不能对此掉以轻心。

为此,我提出下面 3 条建议:

依赖包管理器已基本消除了下载和安装依赖的成本。

未来的开发工作重点更应在于:降低评估和维护依赖的成本。

例如,

世界上优质的软件这么多,为更安全可靠地复用这些软件,我们应携手前行。

参考阅读

做好依赖管理的十五条准则(上)

关于我们

最火推荐

小编推荐

联系我们


版权声明:本站内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 88@qq.com 举报,一经查实,本站将立刻删除。备案号:桂ICP备2021009421号
Powered By Z-BlogPHP.
复制成功
微信号:
我知道了