依赖关系是危险的

文摘   2024-10-31 08:10   上海  

依赖, 我们需要它们,但如何有效安全地使用它们?在本周的节目中,Kris[1]Ian[2]Johnny[3] 一起讨论了 polyfill.io 供应链攻击[4]、Go 中依赖管理和使用的历史,以及 Go 谚语“一点复制胜过一点依赖”。当然,我们用一些不受欢迎的观点来结束本集!

(译者注: polyfill .js 是一个流行的开源库,可帮助旧浏览器支持新浏览器中的功能)



本篇内容是根据2024年7月份#321 Dependencies are dangerous[5]音频录制内容的整理与翻译

过程中为符合中文惯用表达有适当删改, 版权归原作者所有.



Kris Brandow: 欢迎收听 Go Time!又是新的一周,也意味着又有新的一集。我是你们的主持人 Kris,今天我们要讨论的是供应链安全和依赖管理。与我一起参与这期讨论的有 Ian 和 Johnny。Ian,今天你怎么样?

Ian Lopshire: 我很好。

Kris Brandow: 太棒了。

Ian Lopshire: 没什么可抱怨的。

Kris Brandow: 不错。Johnny,你今天怎么样?

Johnny Boursiquot: 我正在检查我们用的东西是否和 polyfill[6] 有关,或者... [笑声] 你知道的,只是做个简单的检查。这就是我现在的状态。

Kris Brandow: 只是简单的检查一下代码库... 所以今天我们要讨论几个话题,但首先我们要谈的是一个漏洞,或者说是一次攻击,可以称之为 polyfill.io 的入侵事件。Ian,你能给听众简单介绍一下发生了什么吗?

Ian Lopshire: 好的,当然。polyfill.io 是一个提供浏览器 polyfill 的 JavaScript 库的 CDN。几个月前,这个 JavaScript 库的维护者警告不要使用 polyfill.io 的 CDN,因为这个 CDN 已经被卖给了另一家公司,而他与之无关。最近,他们开始注入恶意 JavaScript,影响了成千上万的网站,包括 Hulu 和 JSTOR 等等。他们被发现重定向到赌博网站,影响非常大。所以,是的,这是个大事件。

Johnny Boursiquot: 当你说到这个,我就在想 "CDN,CDN"。特别是在前端社区,CDN 几乎是主宰。通常,任何用于构建应用或发布内容的 JavaScript 库都会有一个 CDN URL,因为这是最佳实践。CDN 是全球分布的,用户通常离某个节点很近,这意味着你的网站或应用加载速度会更快。这就是 CDN 的价值所在。但我想很少有人会停下来仔细检查 CDN,因为它们总是在那儿,它们总是有效。你看到一个链接,比如一个 bootstrap 的 CDN 链接,你直接复制粘贴,然后就可以用了。你有了 Bootstrap,或者你想用的任何库。几乎可以说,你默认认为“哦,肯定有人为此买单,我不需要担心它。我只需要把它包含在我的 JavaScript 或应用中,然后继续使用。” 所以,现在由于域名所有权的变化,突然冒出这样的事情,确实很有趣。而且在 npm 社区,虽然这不是 npm 独有的事情,任何使用这个 polyfill 的人都会受到影响。我猜主要是 JavaScript 库受到影响吧?

Kris Brandow: 是的。

Ian Lopshire: 其实不仅仅是 JavaScript 库,至少在这次攻击中,任何包含这个 CDN 的网站都会受到影响。

Johnny Boursiquot: 对,任何人。

Ian Lopshire: 就像如果 jQuery 或 Google Analytics 被攻击或入侵,那会影响多少网站?对吧?

Kris Brandow: 我觉得有趣的是,前端 CDN 提供商的最大优势之一就是,如果每个人都使用同一个 CDN,那么你要使用的库或代码很有可能已经被缓存了,所以甚至不需要向 CDN 发起请求,加载速度超级快。但这也让它们成为巨大的攻击目标,因为它们被包含在那么多的网站上。所以如果你在其中做一个小改动,如果你能进入这个 CDN,你就可以攻破大量的网站。

但在某种程度上,CDN 似乎有点像过去的遗物,当然,我不是说 CDN 本身,而是说用于这种特定用途的 CDN。有点类似于我现在看到的,将资源放在不同的子域上似乎也是过去的遗物... 当我们都在用 HTTP/1 时,我们一次只能有六个连接,你希望所有资源都加载得更快... 而现在有了 HTTP/2 和 /3,我们不再那么需要这种做法,通常你可以把很多东西压缩得非常小,只有在首次加载页面时才会有影响... 所以,也许我们继续使用这么多 CDN 的原因之一是我们已经习惯了,而不是因为我们真的需要它们。尤其是如果你是一个较小的网站,或者类似的情况。所以如果你想在未来更安全一些,你也可以考虑不使用 CDN,直接从你的域名上提供这些资源,或者使用 Google 的 CDN。

Johnny Boursiquot: 那么,如果你是一个 Go 开发者,这会对你有什么影响呢?或者它会影响你吗?

Ian Lopshire: 它不会影响你,但...

Johnny Boursiquot: 不会直接影响。

Ian Lopshire:  不会直接影响。但供应链攻击的概念确实会影响你作为一个 Go 开发者。我认为 Go 已经做了很多工作,让我们不需要太多考虑这些问题,但它仍然是需要注意的事情。我们今年已经看到了其他一些非常狡猾的供应链攻击... 如果你听说过那个压缩库的攻击事件...

Kris Brandow: 哦,xz?

Ian Lopshire: 对。如果你听过 xz 攻击,那实际上是有人基本上对维护者进行了网络霸凌,指责他们更新不够快,然后成功成为了维护者之一... 接着在几年时间里逐渐提交了不同的代码片段,最终导致了这次漏洞。

Johnny Boursiquot: 这真是很复杂的攻击。

Ian Lopshire: 是的,非常复杂。

Johnny Boursiquot: 谁能说霸凌者没有被别人胁迫交出了他们的 GitHub 账号呢...

Ian Lopshire: 这倒是有可能。

Johnny Boursiquot: ...或者其他什么东西。所以可能连原始的维护者都不是。

Ian Lopshire: 考虑到攻击持续的时间,确实有可能。但,是的,百分之百同意。

Johnny Boursiquot: 是的,完全有可能。

Ian Lopshire: 是的,我们也不知道。但---你们在部署和构建代码时会考虑供应链问题吗?

Johnny Boursiquot: 幸运的是,我不会在运行时加载这些东西。即使我是在通过 Go 提供一个应用程序---你可以通过 Go 提供一个 Web 应用程序,对吧?你可以设置一个 HTTP 服务器,提供一个静态网站,或者任何内容... 所以从某种程度上说,如果你通过 Go 提供了一个网站,部分 HTML 或 JavaScript 包含了某个库,并且你在运行时从 polyfill 的 CDN 加载这些库,你可能会受到影响,对吧?这是你可能会直接受到影响的方式。所以你的 Go 代码本身不会受影响,但你提供的前端代码中包含的库可能会受到影响。

因此,对于 Go 本身,我依赖于 Go 团队采取的措施,确保我发布的二进制文件确实是我认为发布的内容。我想你已经链接了关于我们如何在 Go 中防止供应链攻击的帖子。这是 Filippo Valsorda 在 2022 年发布的帖子---我们会在节目笔记中链接这个帖子。所以,基本上,从构建的锁定、版本管理,到构建内容、依赖项的确定性(因为我们使用了校验和数据库),再到使用代码库中的提交哈希作为真实来源,构建代码---构建时不会执行代码,这些都是防止攻击的步骤之一。

另外,总的来说,在 Go 社区中,我们非常谨慎,不会随便引入随机的包... 或者至少我们中的大多数人不会。至少我是非常严格地审视我导入到程序中的任何包的。我们 Go 社区中很多人都非常认同“少量复制比少量依赖更好”这样的理念。

如果我只需要一个简单的库来做某件小事,比如一个实用工具,我宁愿找到一个库,看看他们是怎么实现的,然后实现我自己的版本。或者直接复制我需要的部分,附上引用,将其放在我控制的包中,这样我就知道其中发生了什么。

所以,我认为 Go 开发者非常... 让我这样说吧,这可能不是一个很受欢迎的观点。我认为,平均而言,我接触到的 Go 开发者比普通前端开发者更注重安全性。我认为,通常来说,后端的 Go 开发者比前端开发者更注重安全。这是我的观点,我坚持这个观点。

Kris Brandow: 我觉得这有点像路径依赖。我注意到我自己的变化... 我刚开始写 Go 时,正值那段混乱的时期,那时我们只是“哦,你只需 go get 一下”,然后它会拉取 Git 仓库中 master 分支的最新代码,就是这样。如果它发生了变化,导致某些东西坏了,那么祝你好运。所以,对我们很多人来说,依赖项在某种程度上是一种负担,因为如果你想回去重新构建一些旧代码,而你没有将依赖项打包保存,那么在某些情况下你根本无法构建它,或者你得花几个小时挖出旧的提交,仔细设置你的 GOPATH,确保里面有正确的东西。

所以我认为这促使我们作为一个社区减少依赖项,而且让这些依赖项更加稳定,某种程度上也更小,这样它们的变化就不会那么频繁... 然后我们得到了这些其他的东西,比如版本控制、稳定的构建,以及所有这些加密校验,确保依赖项不会在你不知情的情况下发生变化。我认为这巩固了整个流程。而在 Node 或 JavaScript 社区,他们走的是一条完全不同的道路,他们的路径依赖是“尽可能拉取所有东西,不要重新发明轮子,使用别人已经编写好的东西。” 这种“骄傲地使用别处的代码”的理念非常普遍。如果有人已经写好了某个功能,你就不需要再写一次,这是一种完全不同的依赖项管理方式。我认为这些不同的路径导致了现在完全不同的结果... 这在某些情况下确实让 Go 工程师整体上更加注重安全性。但我不确定这两者之间是否真的有直接关系。我觉得它们可能只是并列存在的东西,但我确实认为我们之所以很少遇到这些问题,是因为我们是一个---我猜可以说是对依赖项不太友好的社区。我们会说“我们的标准库已经很好了。” 依赖项曾经是非常麻烦的事情,这种观念已经深植于社区中,并将持续到未来。这种情况可能在未来会改变,但我认为随着我们引入更多的安全措施,这种观念将会继续巩固。

如果我们从一开始就有一个包管理器,那么我们可能会遇到 JavaScript 社区或其他许多社区正在面对的那些问题,比如包管理混乱无序,需要做大量的修复工作才能回到一个稳定的状态。

Johnny Boursiquot: 我们并不是对漏洞免疫的,对吧?

Kris Brandow: 当然不会。

Johnny Boursiquot: 我一直在反复思考依赖项的问题……很多安全问题和漏洞确实源自于依赖项。每当你引入一个来自第三方的依赖,无论你是否熟悉它---而大多数情况下你并不熟悉它……你可能引入的依赖本身也依赖于其他依赖项;也许你熟悉你引入的那个依赖的作者,但你不熟悉那个依赖所依赖的其他作者……所以每次你引入一个新的依赖时,实际上你是在将一大堆东西带入你的项目。那么,谁能阻止现在或将来某人在这些依赖中引入恶意代码呢?

这样想吧,如果我有一个流行的 Go 包,而有一天---或者某天有人掌握了我的 GitHub 凭证,然后他们决定更新我的包,可能利用 OS 包获取你应用程序中的所有环境变量,就是你导入了我的包的那个应用程序。我读取了所有环境变量,然后发送---或者更准确地说,恶意行为者将这些环境变量发送到某处的服务器。就是这么简单。有人可以轻易地获取你所有的环境变量。如果你遵循 12-factor 应用程序方法(这在 Heroku 时代很流行),通过环境变量配置你的应用程序,而且你在其中存储了秘密信息等,那么谁能阻止某人获取你所有的环境变量并将它们发送到某个随机的服务器,因为你导入了一个你认为只做某件事的库,但事实上它做了更多的事情?

类似的情况让我对我引入的任何包以及这些包所依赖的包都非常谨慎。所以通常,如果我看到一个包中有大量的依赖,特别是这个包并不常用……或者说,如果我在 pkg.go.dev 上查找某个包,我能看到足够的元数据来帮助我做出判断。我可以查看 pkg.go.dev,它会告诉我有多少东西导入了这个包,有多少包被这个包导入……这些因素都会影响我是否导入这个包的决定。尤其是如果这是一个新的包,没有太多活动,没有太多人在使用它,但它却引入了很多东西……看起来它做的事情比表面上更多……对我来说,这些都是我立刻会去检查的信号。我会去这个仓库查看代码,看看它到底在做什么,然后再决定是否引入它。而且这个包的功能必须足够复杂,我自己无法轻松实现。

所以,依赖项可能会对我造成伤害---这是一个非常真实的威胁。尽管 Go 社区有各种检查和防范机制来防止恶意代码的引入,但如果你主动引入了恶意代码,那责任在你。因为你可以去阅读代码,你可以至少去检查一下这个包是否在积极维护,是否是一个流行的包,或者根据你的选择标准进行检查;这完全取决于你……应该花些时间去做这些检查。

当然,我们不是完美的,有些东西可能会被忽视。但你应该养成阅读代码库和源代码的习惯。显然,有些库很大,你可能无法全部阅读。但你可以依赖一些轶事,你可以依赖于这个库有多少人在使用,它已经存在了多久,是否有人在维护……你可以依赖这些信号来帮助你选择依赖项。所以对我来说,随便挑选依赖项并将它们放入你的 Go 程序中---这是一个大忌。

Ian Lopshire: 我认为整个……这曾经是一个有争议的观点……或者说最小版本选择在我们进行包管理讨论时是有争议的。但我确实认为它在这些情况下有所帮助。它使得更新依赖项以及依赖项的依赖项变得更慢了,因为它会引入它能找到的最低版本。但我确实认为这样更安全。版本不会自动更新到可能含有恶意代码的新版本……我觉得这很有趣。

Kris Brandow: 但我觉得这取决于更新的内容……如果更新是为了修复一些漏洞,你肯定不希望使用旧版本。

Ian Lopshire: 这确实是个双刃剑。

Johnny Boursiquot: 但谁会去检查他们程序中导入的每一个包的每一个更新呢?如果项目足够大,你真的会逐条检查所有提交,确保没有引入恶意代码吗?我这么说完全是因为我自己也确实这样做。我会更新依赖项,希望能更新到小版本,以捕捉补丁和安全更新,而不是直接跳到下一个功能版本……但如果项目足够大,你真的会去检查所有提交,确保它们是安全的吗?

Kris Brandow: 我觉得有些东西……我不一定会去检查……如果某个包足够大,我会依赖于“维基百科效应”,也就是说,如果有足够多的人在关注这个包,出了问题,应该很快就会被发现。那些稍微小一些但有一些人使用的包,才是“危险区”。

Johnny Boursiquot: 可疑的。

Kris Brandow: 是的。所以如果它是一个巨大的库,并且有很多人关注它,我就不会太担心,因为即使它是一个更大的攻击目标,漏洞被发现的速度会比其他东西快得多。但我确实觉得……我一直以来都有一个不太喜欢的现象,那就是软件工程的很大一部分已经变成了配置依赖项,而不是自己构建东西……我觉得这确实是一个不太受欢迎的观点:你应该尽量自己构建大部分所需的东西。或者至少你应该足够了解它是如何工作的,这样你就可以去阅读代码,确保它在做你认为应该做的事情。我认为最大的漏洞之一就是当你不知道代码是什么,也不理解它应该做什么时。这时可能会有某种漏洞存在,而你却觉得“哦,这段代码看起来没问题,因为我不理解我试图做的这个底层操作。”显然,这有一个底线。你不需要去阅读 Linux 内核代码,只是因为你想构建一个应用程序。但我认为我们对个人工程师应该审查的内容设定的门槛有点太高了。

我觉得你说得对,Johnny---人们确实需要深入到他们的依赖项以及依赖项的依赖项,去了解那里发生了什么。如果你不想这样做,那也没关系。但你就少引入一些依赖,或者找到一种更好的方式来处理它们。我认为这是我们整个行业需要转变的方向;这份工作的一部分就是处理安全问题,理解你制作的东西里包含了什么。你不能制造产品,也不能向人们出售食品,如果你不知道其中包含了什么。在大多数其他行业中,如果你不知道供应链中的东西,人们会认为这是不可接受的。

我们确实批评过其他行业,比如服装行业,总会有“哦,有些可怕的劳动环境问题。我们需要解决这些问题。”作为公司,你有责任了解供应链中的这些问题;即使它离你有很多环节之遥,你也应该了解这些问题,并尽快解决。我认为我们也处在类似的境地,不管问题出在第几层依赖项上,最终你都要为此负责,所以你需要确保你的供应链和材料清单是干净的。

Ian Lopshire: 说实话,我不会更新依赖项,直到检查工具告诉我有漏洞。你真的会定期检查你的大项目,并更新每个依赖项吗?

Johnny Boursiquot: 我确实做过……对此我有一些“伤痕”。在 OpenTelemetry 的 Go SDK 还在成熟阶段时---那是在我在 Heroku 的可观测性团队工作期间。我们非常早期就开始在 Heroku 采用 OpenTelemetry。所以我们经历了很多变动,依赖项不断变化。突然某个包路径不再存在了,然后我们必须去找出某个类型现在在哪个位置,或者某个特定行为现在被移动到哪个位置……

我们在开始时就知道这一切;我们接受了这些变动,并尽力去做一些贡献,或者理解为什么这些东西会发生变化,并尽量减少对我们的影响……但这确实是一个挑战。当时我们知道我们会长期采用 OpenTelemetry;我们不想创建一个只有我们自己知道的内部机制,因为我们需要更好的可观测性,但我们又不想依赖某种内部的、不透明的东西。因为随着团队的变动,人员进出,他们需要去学习一些难懂的东西。我们更愿意采用 OpenTelemetry,因为这是标准,世界正在朝这个方向发展。于是我们有意识地走向了这个方向。

但也确实有很多小问题,很多小伤口。每当依赖项发生变化时,我们不得不更新代码,找到那些不再有效的引用之类的东西。

所以,是的,对于其他我们尽量保持在小版本更新的库,我们只做补丁,但 OpeTelemetry 是一个例外;每次更新我们都得跟进。

Ian Lopshire: 是的,我认为对于你知道会发生变化的东西,做一些小的改动比一次大规模的破坏性改动更容易。我觉得如果我们从另一个角度来看这个问题会很有趣……如果你想在一个流行的 Go 库中引入漏洞,首先,这是否可能?其次,你需要如何去做?

Johnny Boursiquot: 我首先想到的是,如果你能设法让漏洞数据库,或者某个数据库,在代码仓库内容发生变化的情况下仍然保持相同的哈希值。如果你能做到这一点,你就成功了。因为会发生的情况是---

Ian Lopshire: 这很有趣。

Johnny Boursiquot: 是的。因为会发生的情况是每次新的提交发生时,校验和都会发生变化。所以如果你说“嘿,版本 1.0.2 在这个仓库中,这个提交哈希,一切关于这个仓库的内容在这个时间点都有一个特定的校验和。”然后现在,你设法保持相同的校验和,但仓库的内容已经更改,包含了恶意代码。这样你就成功了。因为每个人都会进行检查,确认“嘿,这些东西对齐了吗?校验和和我们基于它进行的安全检查是否一致?”“是的,你通过了。” 但实际上我们有了新代码---这看起来很不可能。但我们讨论的是那些不应该发生但还是发生的事情,对吧?

Ian Lopshire: 这真的不可能吗?这不就是比特币挖矿的原理吗?我们试图追加数据,直到哈希值符合预期。你不能这样做吗?

Kris Brandow: 我认为现代密码学的基础假设是,这不是不可能,但它几乎是不可能的,碰撞不会发生。

Ian Lopshire: 它可能需要一百万年,对吧?但你可以做到。

Kris Brandow: 我觉得这有点像 XKCD 的那个漫画:某人使用了某种强加密,“我们永远无法破解”,结果却是“用扳手打他直到他给出密码。”[笑声] 与其试图绕过整个系统,不如绕过人类系统,或者在人类系统中找到漏洞。

所以你找一个包,比如 xz 事件中维护者精疲力尽的情况。你提供帮助,慢慢开始参与其中,也许在某个无害的提交中你偷偷引入了一个包。你控制的包,可能并没有做什么特别复杂的事情。然后它在那里待了很久,人们就习惯了它……一段时间后,你在其中加入了恶意代码,可能是在一个小补丁版本中---你只做了个补丁版本更新,没有人真的注意到……尤其是如果这是一个你经常更新的包,也许你添加了很多东西,你一直在构建它……所以它就这样悄悄地混了过去。也许它是你依赖的某个依赖项的依赖项,所以有一些间接性。

然后你在其中加入了一点漏洞代码,它就这样传播到所有地方。它只是另一个人们看到的例行更新。你可能不会去检查代码本身,因为代码本身看起来没问题。你没有在主要依赖项中引入任何恶意代码;它是在某个更远的依赖项中,可能很早就被更新了,现在才逐渐浮现。所以你真的要追踪很多东西。我觉得如果你有一个大的库,包含很多依赖项,某人就有机会在其中偷偷引入一些东西。这比试图破解现代密码学更可行。

Ian Lopshire: 是的,你几乎必须把它隐藏在显眼处---不,实际上你必须把它隐藏在显眼处。我认为这是 Go 的一个优势。比如,对于 C 程序,你通常会发布一个包含编译后内容的压缩包。所以你可以随意隐藏你想要的东西。

Johnny Boursiquot: [笑]

Ian Lopshire: 我是认真的,你可以这么做。

Johnny Boursiquot: 希望它带有校验和。至少可以验证。

Ian Lopshire: 但你不能对 C 代码进行哈希处理,确保压缩包中的内容与 C 代码一致,因为这不是它的工作方式。那是一个预编译的东西。

Kris Brandow: 哦,你的意思是如果你发布了一个---

Ian Lopshire: 比如一个 DLL,或者---

Kris Brandow: 是的,一个 [听不清 00:32:12.00] 之类的东西。

Ian Lopshire: 是的。老实说,我不太清楚它是怎么工作的,但我知道你不会发布 C 代码。

Kris Brandow: 是的,如果你只是发布了对象代码,然后说“哦,只需在编译过程中将其链接起来”,这样它就成为了---

Ian Lopshire: 没错。我们发布源码而不是预编译的部分,这让隐藏东西的空间少了很多。所以你真的得慢慢地在显眼处偷偷引入它。

Kris Brandow: 是的,我觉得这是一个优势。但这也意味着我们不能像 C 代码那样发布……我最近开始重新学习 C 编程,其中一个我想做的项目是用它来控制我的相机……而 Sony 提供了一个 SDK,你可以使用它。他们只是发布了编译好的对象文件。也许它只是一个共享库。但他们会为你的平台发布已编译的代码,这样他们就可以为所有人提供 SDK,而不必开源它……我认为这是一个有趣的代码共享模式,它允许你共享功能而不共享代码……但这确实带来了漏洞,你可以在那个对象文件中放入任何你想要的东西,它可以做任何事情。不过你也可以使用其他代码扫描技术来发现恶意代码。还有其他的安全技术。在你举的例子中,Johnny,如果我获取了所有环境变量并将它们发送到某个地方---那么,如果你阻止你的进程访问随机的 URL,是的,你可能获取了所有环境变量,但你无法将它们发送出去,因为你不被允许访问某些随机的服务器。

所以我认为记住我们可以有其他的保护层,这样即使有某种漏洞,也不一定会影响我们,这一点很重要。我认为在 C 世界里---我不是 C 程序员,所以我不知道他们是怎么做的……但在你不能查看所有可能使用的源代码的情况下,你被迫使用这些其他类型的保护机制。而我们在 Go 中可能不太倾向于使用这些机制,因为我们可以看到所有源代码,所以我们觉得“哦,我们可以查看它。如果有问题,我们可以看到问题所在。”

所以我们可能不会限制我们的进程访问随机的服务器……尽管我们应该这么做。但这是一个运营问题,通常这是运维部门的事,可能会有点烦人……某个开发人员可能会觉得不爽,因为他们想把数据发送到某个随机的分析服务,而他们不想向 IT 部门提交工单来开放……你知道,我们不做这些事情是有原因的……但如果你被迫为了基本的安全性这么做,那么有时这确实能有所帮助。但实际上,你应该默认锁定你的程序,然后为正当理由提供方便的开放方式。

Johnny Boursiquot: 你不允许你的二进制文件随意向任何地方发送 HTTP 请求吗?

Kris Brandow:[笑] 我的意思是,如果我在运行生产基础设施,那当然不会允许这种情况发生。

Johnny Boursiquot:[笑]

Ian Lopshire:我得赶紧记些笔记,可能需要检查几件事……

Johnny Boursiquot就像“为什么这个东西需要联系什么随机服务器?”不过,这又是一个权衡,因为工程师们确实喜欢直接引入依赖……他们会说“哦,我得做这件事,不要挡着我,我得赶紧完成。让我引入这个依赖,它能干这件事。我们要用这个新服务。”然后你问,“你调查过这个服务吗?你确定它可靠吗?”他们会回答,“没有,我们得赶紧完成任务,把它上线。”我听过很多这样的借口。

Johnny Boursiquot:你会问“你正在导入的这个包是什么?它为什么用一个空标识符?它到底在做什么?” [笑]

Kris Brandow:是的……

Ian Lopshire:回到我们刚才讨论的在 Go 中如何在明面上隐藏恶意代码……我实在想不出如何在 Go 中写出一个发 POST 请求的代码,而不让它看起来是在发 POST 请求。至少看起来会很可疑,“你在这儿做了一些奇怪的事情。” 没有太多办法可以隐藏这种东西。

Kris Brandow:嗯,如果你足够聪明……

Ian Lopshire:我肯定有办法,但我现在想不出来。

Johnny Boursiquot:我的第一步---在我的验证工作流中,包括查看 pkg.go.dev 以及它的元数据,如果我深入挖掘的话,当我查看某些依赖项的仓库时,无论是直接依赖还是间接依赖,我通常会在仓库里快速搜索一下是否有 HTTP 包。如果我找到……[笑] 那么我会对这个包非常感兴趣。如果这个包只是用来,比如说,反转字符之类的东西……那为什么这里会有 HTTP 包?你就得开始质疑了。“这个包应该只做这件事,为什么它还要进行网络请求?发生了什么?” 这些事情应该是明显的警示,提示你需要深入调查。这就是我所说的,我们不可能抓住所有问题。无论我们的流程多么严格,我们认为自己已经跳过了很多环节,以避免漏洞,但安全始终是一个减轻风险和设置防护措施的游戏,它不是你可以百分百做到的事情。因此,作为开发人员,你实际上是第一道防线,你应该对这些事情保持批判态度。我正在导入这个包,它应该做这件事……为什么它还要做别的事?

Kris Brandow: 是的。不过,如果我们重新从攻击的角度思考,如果我要建立某种漏洞,我不会用 HTTP 包。我可能会用 OS 包,做一些系统调用,设置一个套接字……

Ian Lopshire:系统调用……[笑] 是的,正如你所说的……我看到这个就会觉得“这看起来很奇怪,为什么这个字符翻转工具需要进行系统调用?”

Kris Brandow:或者你说“哦,这个字符翻转需要非常快,所以我们用汇编语言写它”,然后你在汇编代码中做所有系统调用的事情。你能看出区别吗?你能分辨出是字符翻转还是在汇编中设置套接字吗?

Ian Lopshire:我觉得我能分辨出来。我真的觉得我能。

Kris Brandow:也许吧,也许吧。

Johnny Boursiquot:你简直就是个巨人,Ian。 [笑]

Ian Lopshire:我觉得,汇编代码的长度,至少从这一点上来说,应该会有明显的区别。但我是谁来下这个判断呢……

Kris Brandow:嗯,这取决于情况。也许你在其他地方有某个文本块,它是一些混淆的代码,实际上展开成更多的汇编代码来做一些事情……你可以做一些深层的嵌套,真的可以尝试欺骗一些人,如果你真的想这么做的话。但说实话,这是一项庞大的工作,与其这么麻烦,你还不如直接用扳手来解决问题……还有其他方法可以利用人类的弱点。

Johnny Boursiquot:没错。这些显而易见的事情……有些事情……如果某个人足够有动机,你真的很难找到所有漏洞。那些简单的东西,比如脚本小子(script kiddies)常用的东西。简易的漏洞,“让我用 HTTP 包把这些数据发送到我地下室的服务器。” 这些简单的东西你至少应该能快速识别出来。

不过,我相信一定有一些公司专门从事这种事情,比如安全公司。他们可以扫描你的二进制文件,我相信也会有一些解决方案可以引入。如果你真的想要额外的安全保障,你可以请第三方来做这件事。

Kris Brandow:是的,因为到了一定程度,你可能会发现“哦,我不能直接攻击代码。哦,你现在用的是 Kubernetes。也许我可以通过攻击 Kubernetes 来入侵。” 所以一旦你把一个部分防护得足够好,攻击者会转向另一个部分进行入侵,造成破坏。

我认为在某种程度上,很多都是关于威慑,像在你的系统里到处设置威慑机制,让它变得越来越难攻破,这样攻击者最终会去攻击其他目标,因为你太难对付,不值得花时间。希望我们都开始这样做,最终大家都处在一个更好的位置,但这并不一定会发生。你不可能一下子就修复整个行业。不过,在我们进入下一个话题之前,关于依赖项和漏洞的讨论,还有什么最后想说的吗?

Johnny Boursiquot:一点点复制总比引入一点点依赖要好……[笑]

Kris Brandow:对。

Ian Lopshire:我只是想说,Go 的安全数据平面和漏洞数据库做得非常好,而且有很好的机器人可以扫描这些漏洞……所以如果你只是想迈出第一步,只需把这些工具集成到你的 CI 系统里,然后按照提示更新依赖项。我觉得这能起很大的作用。

Kris Brandow:听起来不错。现在到了我们节目中最喜欢的小环节---不受欢迎的观点时间。

Kris Brandow:好了,谁有不受欢迎的观点?

Johnny Boursiquot:我再重复一次我之前的观点。我认为,总的来说,后端开发人员比前端开发人员更注重安全。原因有很多---我们刚才讨论了很多,但纯粹从 npm 世界不断冒出来的漏洞数量来看……我觉得这很明显。再说一次,我在这里是泛泛而谈,但在我看来,似乎前端开发人员更容易仅仅复制粘贴代码片段,然后就假设一切都已经照顾好了。某个 CDN 上的某个库,或者某个从 CDN 引入的库……当然了,这是我们社区里经常做的事情。很多假设是“哦,是的,所有东西默认都是安全的,我不需要特别关注它。” 很少听到前端开发人员对这些问题感到担忧。当然,这并不是说前端社区里没有人会关心这些问题……我只是说,在过去几年里,无论我在哪个后端社区,总有一些人会特别关注依赖项和安全性等问题。而我在前端社区里感受到的这种关注相对较少。

Kris Brandow:嗯,我不确定。我确实觉得后端人员……是的,我觉得因为后端需要做很多安全工作,要确保它的安全性,而且它是一个很大的攻击目标,所以我认为后端人员确实会更关注这些问题,而不太会关注其他方面。相比之下,前端开发人员主要关注的是让前端工作起来,还要应对不同浏览器之间的差异,以及这些浏览器之间发生的各种奇怪现象。

我觉得这也是控制权的问题,你在后端有更多的控制权,而在前端则没有。归根结底,你的代码是在别人的电脑上运行的,运行在某个浏览器之上……你没有办法完全控制这一堆东西,而在后端你对自己的技术栈有更多的控制权,也能更好地掌控发生的事情。

Ian Lopshire:我确实认为后端开发人员应该更加注重安全。想想其中的风险……一个后端漏洞可能会泄露整个数据库。前端出问题,可能只是影响一个用户。

Johnny Boursiquot:我试图访问 JSTOR,结果它把我重定向到了 casino.com,或者别的什么地方。 [笑] 这也不是无关紧要的……

Ian Lopshire:对,这确实不是无关紧要的。而且我确实认为两者都应该关注安全问题……但如果我必须选择让一个用户受到攻击,我宁愿它是单个用户,而不是我的整个数据库。

Kris Brandow:我觉得这也部分源于,后端代码是在你的安全域内,而前端代码不在,所以你得把前端代码当作潜在的敌对代码来处理。所以在某些情况下,这也降低了对前端安全的需求。大多数时候,很多前端漏洞,比如跨站脚本攻击(XSS),也可以通过后端来阻止或预防。不过我确实觉得前端开发人员缺乏安全意识,尤其是缺乏加密技术的意识,这确实在某些方面有所妨碍。我注意到浏览器中的加密 API 并不多见,而实际上这可以成为一种更好的身份验证和授权方式。浏览器已经有了这些 API 好几年了,你可以在浏览器中创建一个公私钥对,其他脚本甚至在你的子域上都无法提取这个密钥。这可以让你以一种加密安全的方式唯一标识这个浏览器,你不需要传递可以被窃取的令牌,而无论如何,某人都可能在你的网站上运行一些 JavaScript 代码。

是的,通过跨站脚本攻击你可以做一些 API 请求,但你不能窃取那个密钥并发送到服务器,然后开始大量请求,或者做任何那样的事情。相比之下,你可以用 [无法听清 00:46:18.05] 令牌做到这一点。我觉得这种情况出现的部分原因是前端社区对这些工具和理念不太熟悉,所以对这类东西的支持不多。虽然我们已经开始有了像 passkey 这样的东西,但相比于我们本可以更早开始做的事情来说,这确实是太晚了。

Johnny Boursiquot:这可不算是不受欢迎的观点。我都不知道 passkey 是怎么工作的。我最近访问的每个网站都在提示我使用 passkey,我就想“哦。” 我好像曾经读过微软的某些东西……

Kris Brandow:简单来说,它就是我刚才描述的。它是非对称加密。也就是公私钥对,公钥注册到了服务器上,当你登录时,它会签署某个东西并发送到服务器。这个过程就是为了确认你确实持有这个 passkey。

当然,还有很多其他事情需要处理,像是共享 passkey 之类的东西,来实际实现它们在不同地方之间的移动……但最简单来说,它就是将公钥加密应用于认证过程。

所以,是的,这很像我们用 SSH 时做的事情,或者很多其他东西,或者 TLS。当你使用双向 TLS 时,它做的也是同样的事情。所以这只是把它应用到浏览器上。Passkey 很棒,我很喜欢 passkey。

Johnny Boursiquot:很棒,除了没人知道怎么用它。我经历过几次设置过程……但我说不清楚设置过程中发生了什么。而且当我为 Google 设置时,和 Apple 的设置稍有不同,微软的设置也不太一样……它们似乎有些微小的差异,至少它们帮助我设置 passkey 的体验略有不同。有时用手机,有时用笔记本……老实说,我不知道它是如何工作的。也许几年后……

Kris Brandow:我用的是 1Password。所以这些都集成在一起了。

Johnny Boursiquot:我也是,没错。

Kris Brandow: 所以 1Password 就像是“你可以设置一个 passkey。” 但本质上,这和我们用 Yubikeys[7] 做的事情是一样的。我认为设置 Yubikey 的流程和设置 passkey 的流程是一样的。它们实际上是一样的。但确实,不同的平台上略有不同,但本质上做的事情是一样的,“这是一个公钥,它标识了我是谁。好的,太棒了。” 显然,里面有很多细节和微妙之处,但基本上就是这么回事。你给他们一个公钥,证明你是谁,然后他们稍后会要求你证明这一点。但我很高兴我们正在尝试淘汰密码,这让我非常开心。不过它们---

Johnny Boursiquot: 听着,密码会成为科技界的传真机。它们永远不会消失,永远不会。

Kris Brandow: [笑] 是的……

Johnny Boursiquot: 某处的某个人---就像你老派的牙医诊所,或者其他地方,你需要发送一些保险证明,等等……他们会说“你能传真给我们吗?”我就像“什么?!你什么意思?”

Kris Brandow: 就像,我们能不能---

Johnny Boursiquot: “你觉得我家里还有传真机吗?” [笑]

Kris Brandow: 我确实不得不给政府传真了一些东西,我当时就想“传真?传真……?!好吧……”密码永远不会完全消失,但它们可以在绝大多数情况下被有效淘汰。我们已经有社交登录很长时间了。现在我们有了 passkey。而且你现在也有了密码管理器---去用一个吧。

Johnny Boursiquot: 现在普通的非技术人员才开始理解它们是什么以及为什么要使用它们。我不指望我七十多岁的母亲,天佑她的灵魂,知道如何使用密码管理器。你在开玩笑吗?我得手把手教她多少东西啊……

基本上,我必须告诉她,“看,每个网站都用一个随机密码。是的,我知道这很痛苦,但把那些密码安全地保存在手机上,记在笔记本里,或者别的地方……然后不要在每个地方使用相同的密码,而且要非常小心。如果有人在任何时候要求你发送密码,立刻给我打电话。” 这些是我为她设的安全措施,因为我不能指望她使用密码管理器。这太复杂了。

Kris Brandow: 也许这是个不受欢迎的观点,但我认为任何能用电脑的人都可以学会这些东西。我认为我们引入它们的时间太晚了。如果我们从 20 年前就开始推行这场 passkey 革命,就像我们本可以做的那样,我认为每个人都会习惯它……就像“哦,你还在用密码吗?这太奇怪了;你应该用我们有的其他东西。” 但我们只是没有建立起相应的基础设施。

我认为推动 passkey 向前发展的东西是我们手机和电脑上的生物识别技术,现在你可以真正确认坐在电脑前的人就是你认为的那个人,达到一定程度的确定性。但我认为,很多这类问题之所以难以理解,是因为我们解释得太差,而不是因为人们从根本上无法理解。我敢肯定,你妈妈不记得每个人的电话号码,但我打赌她已经学会如何使用手机上的通讯录应用,把所有号码都放进去,然后说“哦,我要给这个人打电话。让我点一下通讯录里他们的名字。”

Ian Lopshire: 我不同意这一点。 [笑声]

Johnny Boursiquot: 你妈妈能记住---她能记住她遇到的每个人的电话号码吗?

Ian Lopshire: 不,她只是有一堆随机的号码在她的短信里,像“我不知道这是谁,但我跟他们聊过。” [笑声]

Johnny Boursiquot: 哦,天啊……

Kris Brandow: 我的意思是,总会有一些人---

Ian Lopshire: 我脑子里并不是想着我母亲,但我确实有一个特定的人在想。

Kris Brandow: 总会有一些人会做一些奇怪的事情,像“哦,这是某人的号码,但我不知道是谁的……” 但我会说,绝大多数人都懂得如何使用他们手机上的通讯录……这实际上是非常相似的事情。就像“哦,去你的通讯录里查一下。好的,去你的密码管理器里查一下,点这个,点那个。” 好吧,这是个跑题了。

Ian,你有什么不受欢迎的观点吗?

Ian Lopshire: 是的,这个观点可能会非常不受欢迎……它与我们刚才讨论的内容有关。我认为你不需要更新依赖项,除非你有理由这么做。

Johnny Boursiquot: 嗯,是的。这是公平的观点。

Kris Brandow: 我觉得这是……字面意思是你只需要---我不知道这怎么会是不受欢迎的观点。你什么意思?你认为我们会无缘无故地更新依赖项吗?

Ian Lopshire: 我的意思是,是的。

Johnny Boursiquot: 的确有人这么做。是的。

Ian Lopshire: 有人会这么做。有人每月都会更新所有依赖项。

Johnny Boursiquot: 是的。

Ian Lopshire: 我觉得这是浪费时间。除非有理由,比如有漏洞、已知漏洞,否则我不认为有必要主动更新依赖项。

Kris Brandow: 除非是 API 发生了变化,或者你需要某个新功能……但如果你只是为了保持所有东西都更新而去更新……公平地说,也许作为反驳,我会说我更希望企业这么做,而不是他们现在做的事情---从不更新任何东西,然后当有漏洞时,他们也不更新,最后被黑客攻击。所以我宁愿他们有一个流程,始终更新所有东西,这样当有漏洞时,他们至少知道如何更新。因为如果你不经常更新,可能就会在需要更新时不知道该怎么做。

我们进行火灾演习的原因是你需要让人们知道在真正的紧急情况下该怎么做,而不是在紧急情况发生时才去搞清楚。

Ian Lopshire:  我在其他语言中并不持这种观点。比如,如果我在做 JavaScript 项目,我会想“是的,我可能应该时不时更新这些东西”,因为我不知道你有没有不更新 React 五个版本,然后 [无法听清 00:54:26.25] [笑声]

Kris Brandow: 还不如直接把整个项目扔掉,重新开始。

Johnny Boursiquot: 是的,重新开始。

Ian Lopshire: 让我具体说一下……对于 Go 项目,我不认为你需要主动更新依赖项,除非有已知漏洞,或者你需要新功能。

Johnny Boursiquot: 是的,很多这类问题取决于你所依赖的项目维护者的纪律性。我遇到过这种情况,某个依赖项的安全补丁被添加到我未使用的升级版本中……这意味着如果我想要这个补丁,我必须升级并获得补丁。所以如果他们没有足够的纪律性,或者坦白说没有足够资源或时间为每个版本打上这个补丁---这确实是个劳动密集型的工作---那么你最好的选择可能是强制升级,这可能会在你实际获得补丁之前引入一些可能破坏你项目的变更。所以你并不总是有选择的余地。你依赖你的依赖项,这也是为什么我不喜欢有太多依赖项。

Ian Lopshire: 这也是因为我在我工作的项目中几乎没有什么依赖项,所以我可能有点偏见。

Johnny Boursiquot: 祝贺你。 [笑] 你真是幸运。

Kris Brandow: 好的。我的不受欢迎的观点---我觉得这可能会非常不受欢迎……但我认为每个人都应该学习 C。也许不是所有东西都用 C 来编写,或者用 C 写很多东西,但我认为每个人都需要学习 C。我认为这是我们这个行业需要做的事情。因为我觉得人们对这些东西的理解远远不够。我认为太多语言做得太好,把所有东西都隐藏起来了。

Johnny Boursiquot: 为什么是 C 呢---是为了让人们自己管理内存吗?你希望他们从中学到什么?

Kris Brandow: 是的,我认为 C 是为数不多的基本上接触到硬件的语言之一。它几乎是最底层的。你能去的最低层是汇编语言……除非你出于某种原因想手动编写机器码。但有汇编语言,然后紧接着就是 C 语言。所以你在 C 中能得到很多其他语言中的高级设施,但你必须了解机器的工作原理才能写 C,或者甚至学会 C。如果你想写好 C 或者其他没有这种过程式层次的语言,比如那些更面向对象、抽象程度更高的语言,你至少需要在某种程度上理解你的指令是如何流动的。

Johnny Boursiquot: 对我个人来说,自从我开始用 Go 编程以来,我不再想念面向对象编程了。有些人会说“哦,Go 也是面向对象的。” 这是一个争论已久的话题……但我觉得写 Go 给我的感觉更像是在写 C,而不是其他语言。所以我会说,如果你从未接触过 C,而你想知道这是什么感觉,或者一些人仍在写 C 代码的感觉是什么样的,那你应该去看看。至少写个 Hello World,或者比这更复杂一点的东西,来感受一下。看看自己分配内存和释放内存的感觉。你会感受到你从现代语言中获得的好处。

至于面向对象编程、多个抽象层次、抽象类型以及所有这些东西……我并不怀念这些东西……在我的 Go 程序中,我最想要的抽象就是使用接口。这是我在 Go 中想要的所有面向对象或抽象的极限。

Ian Lopshire: 我本来要说,我不确定是否每个人都应该学习 C,但每个人都应该尝试类似 C 风格的语言;比如 Go、C,类似的东西。我这么说是因为---作为一个在职业生涯的头几年里写 PHP 和 JavaScript 的人,我完全不知道 HTTP 请求其实就是通过字节传输的。在 JavaScript 中你有对象,在 PHP 中你有这些大的关联数组,我当时---我完全不了解它们是如何在彼此之间转换的。对象就是它们本来的样子,我根本没有概念,一切都是字节。当你没有这个核心概念时,你会犯很多愚蠢的错误。所以在较低级别做一些事情确实很重要,即使你只是在那些更高级、抽象程度更高的语言中工作。

Kris Brandow: 是的。我之所以特别强调 C,首先,C 是所有系统的通用语言。如果你想让两种语言之间进行交互,最终你是通过 C 来实现的。但我也认为,也许我们还有其他语言可以展示更多机器工作原理的基本知识,这也会很有帮助。因为我认为 C 的一个特点就是它没有任何东西---不像在 Go 中,你可以在函数前面加上 go,它就会启动一个 goroutine。“太棒了,并发。” 在 C 中,你得弄清楚你要做的事情是如何工作的,还有多线程是如何工作的……但你也需要了解缓存行是什么、我的 CPU 是如何工作的、所有这些东西是如何运作的。我认为现在有很多人不了解这些东西,结果我们写出了很多糟糕的软件。

Johnny Boursiquot: 因为我们被告知我们有无限的内存、无限的磁盘空间、无限的 CPU……只要再加一个虚拟 CPU 就行了……我们被训练成这样去思考,“哦,是的,不用担心这些问题。我们会投更多硬件进去解决。” 而在过去,我们没有这样的奢侈,对吧?

Kris Brandow: 人们对---我们提到了内存管理,但对垃圾回收也是这样。总有人说“哦,我们有垃圾回收问题。” 其实你没有垃圾回收问题,你有垃圾问题,因为你创造了太多垃圾,导致垃圾回收器跟不上。你只是制造了太多东西,然后把它们丢掉了。

我觉得现在仍然有一种持续的误解,认为在不需要手动管理内存的语言中你就不需要做内存管理。其实不然,你仍然需要做内存管理,你仍然需要思考你的内存分布方式,你在做什么,你有多少内存,特别是在设计应用程序时。但我认为通过深入研究 C,你可以学到很多东西,并对更高级的语言有足够的熟悉度来更好地使用它们。

Johnny Boursiquot: 我不反对。

Kris Brandow: 好吧。今天的节目就到这里了,感谢大家的收听,确保你的依赖项都没问题。不要让你的代码里有任何漏洞代码,拜托了。拜托了。

Johnny Boursiquot: 赶紧检查一下你所有的前端代码。如果你发现了 polyfill.io……

Ian Lopshire: 赶紧删掉,赶紧清理掉。

Johnny Boursiquot: 别走,跑!跑去你最近的版本控制系统。 [笑]

Kris Brandow: 甚至周五也可以做。这是一个周五部署的理由。无论如何,感谢你们,Ian 和 Johnny,今天的节目很愉快。也感谢听众朋友们。下次再见。

Johnny Boursiquot: 再见,大家。

参考资料
[1]

Kris: https://github.com/skriptble

[2]

Ian: https://github.com/ianlopshire

[3]

Johnny: https://github.com/jboursiquot

[4]

polyfill.io 供应链攻击: https://blog.qualys.com/vulnerabilities-threat-research/2024/06/28/polyfill-io-supply-chain-attack

[5]

#321 Dependencies are dangerous: https://changelog.com/gotime/321

[6]

polyfill: https://github.com/polyfillpolyfill/polyfill-service/issues/2873

[7]

Yubikeys: https://en.wikipedia.org/wiki/YubiKey


旅途散记
记录技术提升