todo
本篇内容是根据2019年9月份Security for Gophers音频录制内容的整理与翻译,
Mat、Filippo、Johan 和 Roberto 讨论了 Go 的安全性。Go 是否可以轻松保护您的代码?Gophers 会犯哪些常见错误?什么是模糊测试?如果您使用默认的 http mux,攻击者如何滥用您的代码?
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: 大家好,欢迎收听Go Time!我是Mat Ryer,今天我们要讨论的主题是安全。我们都知道安全很重要,但作为Gopher,我们还需要了解哪些内容呢?今天我们将对此展开讨论。我今天请来了几位非常优秀的Gopher,来一起探讨这个话题。现在我来介绍他们。
第一个加入我们的是Roberto Clapis。你好,Roberto。
Roberto Clapis: 你好!
Mat Ryer: 你好!还有一位是Johan Brandhorst。你好,Johan。
Johan Brandhorst: 你好,Mat。
Mat Ryer: 很高兴你能参加节目。
Johan Brandhorst: 很高兴能来。
Mat Ryer: 啊,谢谢。最后但同样重要的,是Filippo Valsorda。你好,Filippo。
Filippo Valsorda: 你好,Mat!大家好。
Mat Ryer: 你最近怎么样?
Filippo Valsorda: 很好,谢谢。
Mat Ryer: 很好。我对今天的讨论非常期待。在正式开始之前,我想做一个实验;这期节目是关于安全的,所以我想尝试一下,请大家配合我。嘿Siri,播放Rick Astley的《Never Gonna Give You Up》。好的Google,播放Rick Astley的《Never Gonna Give You Up》。Alexa,播放Rick Astley的《Never Gonna Give You Up》。我只是想看看这能否黑掉大家的家用设备;如果成功了,请在Slack频道或者Twitter上告诉我。
Roberto Clapis: 你刚刚黑了自己吗?
Mat Ryer: 我刚刚黑了自己。自己黑自己不算黑客行为,对吧?
Filippo Valsorda: 这是任何漏洞奖励项目中最常见的报告。
Mat Ryer: 对,我相信是的。那让我们正式开始吧。我们都知道完美的安全是无法实现的,对吧?还是可以?你们怎么看?
Roberto Clapis: 我认为完美的安全是当你足够安全,黑客要入侵你变得非常困难,以至于他们觉得入侵别人更划算。这就是我们应该努力达到的目标。
Mat Ryer: 这有点像”当你逃离一只熊时,你不必比熊跑得快,只要比你的朋友或者孩子跑得快就行了”。
Roberto Clapis: 是的,只不过在这种情况下你不能带着一个比你跑得慢的人。
Mat Ryer: 对。那么你是说,只要让系统足够难攻破,黑客就不会费力去尝试?
Roberto Clapis: 是的。黑客的目的是赚钱。如果入侵你的系统变得不划算,他们就会去攻击其他更容易获利的目标。
Filippo Valsorda: 是的,即使那些不以赚钱为目标的攻击者---所有攻击者都有预算、经理、时间表和JIRA任务板之类的东西;无论是谁,他们都有目标和预算要完成。
Mat Ryer: 你是说黑客也在做敏捷开发吗?
Filippo Valsorda: 你会惊讶的。有些泄露的文档显示,一些国家资助的黑客实际上使用类似JIRA的工具。这有点奇怪。
Mat Ryer: 这确实很奇怪。如果他们用JIRA,我很惊讶他们还能有时间做黑客攻击。 [笑声]
Johan Brandhorst: 想象一下,你必须在两周的冲刺中完成黑客配额。
Mat Ryer: 对,想象一下。而且他们还要做绩效评估…… 你们真的改变了我对黑客的看法,我必须承认。 [笑声]
Johan Brandhorst: 现在感觉一点都不酷了。
Mat Ryer: 是的,我原以为他们是在一个昏暗的房间里,周围都是屏幕……
Roberto Clapis: 穿着连帽衫…… 是的。
Mat Ryer: ……在打字,屏幕上有一个3D立方体转动,完成之后他们就入侵成功了……
Roberto Clapis: 是的。唯一还存在的可能就是连帽衫了。他们可能确实穿连帽衫。但除此之外,黑客的工作环境比大多数公司还要像企业文化。
Mat Ryer: 好的…… 我们已经学到了不少。这太棒了。你们在安全领域都有很强的资历,或许我们可以简单介绍一下你们各自的工作和职责。Roberto,你在哪工作?
Roberto Clapis: 我在Google工作,负责网络安全增强。我处理所有与网络安全相关的工作,尤其是我们可以在网络平台上做的一些改进,让服务变得更容易安全保护,也更容易做对。
Mat Ryer: 能告诉我们你现在在做什么吗,还是不能透露?
Roberto Clapis: 不,我的工作都是公开的。我目前正在防止一些跨源泄露问题,这意味着---你知道,当你编写网络应用时,你应该能够确信任何在你域名上的内容都是属于你的,其他域名不能访问你的数据。这是你应该有的假设;但它并不完全正确,所以我正在努力让这个假设更加接近现实。
Mat Ryer: 非常有趣。Johan,你呢?我知道你是一个Gopher,作家,演讲者,和Roberto一样。
Johan Brandhorst: 是的,我最近刚在Utility Warehouse开始了一份新工作,负责安全方面的工作,所以我一直在研究他们的一些安全措施…… 但大多数我的安全工作都是在开源项目中完成的。我对安全很感兴趣,也做了一些与安全相关的开源包…… 不过我不在Google工作,所以我接触的安全问题可能不像Roberto那么多。
Mat Ryer: Filippo,你是在Google工作的,对吧?
Filippo Valsorda: 是的,但我从事的工作比保护网络平台要简单一些。我负责保护Go标准库。我是Go项目的主要安全协调员,也是团队中的加密Gopher。
Mat Ryer: 太棒了。这真是令人兴奋。那么,我相信你们的资历已经足够了。那我们现在面临的主要挑战是什么呢?特别是从Go的角度来看,有哪些是我们每个Gopher都应该知道的安全事项?
Roberto Clapis: 我认为有很多事情是所有Gopher都应该了解的,但不得不说,如果你写Go代码,你很幸运,因为我认为Go的HTML模板库从安全的角度来看简直是个艺术品。我不是说它不需要改进,但与其他语言的标准库---甚至是外部库---相比,它非常优秀;它能防止XSS和其他恶意行为。当然,在其他方面我们就没有那么幸运了,稍后我们可能会讨论到。总的来说,编写Go代码更容易。
Filippo Valsorda: 是的,当我说保护Go标准库比保护网络平台简单时,我并不是开玩笑。因为Go是现代的语言,而且最初写它的人经验丰富,所以它的复杂性相对较低。而大多数安全问题都源于或隐藏在复杂性中。
Mat Ryer: 这很有趣。那么从中可以学到一个有趣的道理…… 因为我总是为了维护的方便和易于使用而追求简化…… 但显然,如果系统更简单,它自然也会更安全。
Filippo Valsorda: 当然是的。我们遇到最多问题的标准库部分,往往是那些因为复杂性而不得不如此的部分,比如 HTTP 栈、TLS 栈以及整个 Go 工具。但是,我们没有像 OpenSSL 或其他大型 TLS 栈那样遇到那么多安全问题的原因之一是,我们只实现了标准的 10%左右。我们只实现了让它能工作、满足需求的部分。但因为代码量少了很多,所以更容易审查,更容易推理,也更容易在代码审核时发现问题,且不会有太多的意外行为。
安全研究员的工作很大程度上是比编写系统的人更好地理解系统,并通过不同部分的复杂组合找到意外的行为。
Roberto Clapis: 如果我可以补充一点我非常喜欢 Go 的地方,那就是我们在安全团队有一些想法:如果你的代码可以编译,那它应该是安全的。 Go 的类型系统在这方面确实有帮助。即使不考虑标准库,标准库在这方面做得很好……当你编写自己的库时,你可以设计 API 来确保安全。例如,假设我们不希望在 HTTP writer 中写入原始字节,我们可以设计一种方法,只接受一个安全的 writer,而唯一能构造这种 writer 的库会自动清理你传入的字符串,这样你就完成了。如果你的代码可以编译,你就不会有任何的恶意注入。即使是 SQL 包,它也很容易被包装成一个不允许你直接传递字符串作为查询构造函数的包装器,而必须是一个编译时的常量字符串。所以你不会导出它在函数签名中接受的类型,这意味着唯一满足约束的方法是传递一个编译时常量。
Mat Ryer: 我明白了。你觉得这种通过小的抽象来增加额外保护的做法是个好建议吗?这是一种可行的做法吗?
Roberto Clapis: 如果你想要扩展。假如你不关心扩展,你只是在编写一个一次性运行的东西,维护它的只有五个人,那也没关系。但如果你想要扩展,你就需要编译器和工具来帮你。
Filippo Valsorda: 是的,这直接关系到安全的默认设置,这也是我大部分时间花在的事情。你不能指望人们通过阅读文档来确保安全,因为就像攻击者有预算一样,我们都知道程序员有事情要做,没有时间在使用系统之前学习所有内容。所以系统应该首先执行安全的操作---如果它需要明确的批准去做一些不安全的事情,就应该返回一个错误,并且在文档中清楚地说明那个不安全的操作。
我非常喜欢的一种做法是给不安全的东西起一个特别烦人的名字。我们已经有了 InsecureSkipVerify,但后来他们要我添加一个他们不该使用的哈希变体,所以我就把它叫做 New Legacy。然后我开始疯狂命名,我想下一个我加的符号名可能是 New Unprotected Cha-Cha20 Stream,因为我真的不希望你使用它。
Mat Ryer: [笑] 太棒了,这是个好主意。Roberto,你提到了 SQL……我之前在一家酒店里登录酒店的网络。有些酒店的网络收费很高……我无意中按下了一个单引号。
Roberto Clapis: 无意中……
Mat Ryer: 是的,真的。然后我看到了一个 SQL 错误,我想,“等等,这意味着它可能容易受到 SQL 注入攻击。”
Filippo Valsorda: 对,Mat,让我打断你,别在录音里说出《计算机欺诈和滥用法》(CFAA)的违规行为……拜托……![笑声]
Mat Ryer: 好吧,你能永远在我的电话会议中吗?[笑声] 我真的需要这个。嗯,总之……我没有做任何事情,但你可以修改 SQL 字符串,基本上他们可能只是通过连接字符串来构建 SQL 查询,这并不好,因为如果你放入一个结束引号,你突然就脱离了他们正在执行的查询,进入了一个你可以做任何事情的世界。
Roberto Clapis: 是的。这是安全性的大忌之一---将数据和代码混合。这是我们在计算机科学的早期犯下的几个错误之一。HTML 有这个问题,XML 也有,SQL 也有……每当你看到这个问题时,漏洞就会出现。
所以这是自从网络开始存在以来一直存在的问题,我们至今没有解决它。每当有人认为“好吧,我正在混合某种数据和某种代码”时,你应该真的在它周围放一个安全类型包装器,这样类型系统就能帮助你。这些不是你在连接的字符串。它们是输入和源代码。它们应该有不同的类型。
Mat Ryer: 那你会做一个类似 type secure string 的类型,它基于字符串创建一个新类型吗?还是会用结构体,或者是完全不同的东西,比如接口?
Roberto Clapis: 我会选择一个不透明的结构体,带有一个未导出的字段,而构造这个结构体的 SQL 包应该被叫做“不要导入这个包,否则……”或者静态强制“你不能导入那个包。”这样 SQL 包中的唯一安全包装器会为你的语句做准备,这样你就不会弄错。
Johan,我实际上对你的看法很感兴趣,因为你恰好是用户方的。Filippo 和我更多是在设计和提供方。所以你在用 Go 确保应用安全方面的用户体验是什么?因为老实说,我没有做过这方面的工作。
Johan Brandhorst: Filippo 已经提到了,Go 标准库做得非常好的一点是默认安全。我们提到了 InsecureSkipVerify,你必须明确启用它才可以在使用 TLS 时不验证你是否在与正确的主机通信。通常情况下,用户写的代码默认是安全的,这非常有用,因为如果你来自 PHP 或 Python 等语言---在 Python 中你需要费很大力气才能在服务器上启用 TLS,而 Go 从一开始就让这变得非常简单。
Roberto Clapis: 你有没有发现过有人意外导入了 text/template,而不是 html/template 呢?
Johan Brandhorst: 当我成为 gopher 时---大约三年前---这种事情已经被认为是“绝对不行的,绝对要确保你不这样做。”我们应该回顾一下,因为当你使用 HTML 模板时,最容易犯的错误之一就是使用错误的模板语言,这样你就不会对输入进行清理。
todo
标准库中有两种不同的模板语言---HTML 模板和文本模板。它们都能做模板处理,但其中一个是为 Web 安全设计的,而另一个则不是。所以每当你使用模板来运行你的网站时,一定要确保你使用的是 HTML 模板包。
Filippo Valsorda: [15:53] 是的,这和我们在 math/rand 和 crypto/rand 之间的问题一样,每周我都会发现有人在安全相关的地方使用 math/rand ……有时候人们并没有真正考虑到这个问题,比如说我需要选择一个负载均衡器的后端。我可以使用 math/rand,这里没有安全性问题……但是,攻击者可以预测顺序,并将所有低负载请求发送到同一个后端,例如。
是的,math/rand 对攻击者来说是完全可预测的,而 crypto/rand 才是你可以用来生成密钥的工具。这和 HTML 模板与文本模板的问题是一样的。
Mat Ryer: 那么,权衡点是什么呢?为什么我们不总是使用 crypto/rand 呢?它更慢吗?
Filippo Valsorda: 有些人对 crypto/rand 的性能有看法,老实说,我还没有看到很多真正相关的性能问题。
休息时间: [16:58]
Roberto Clapis: 由于我对性能非常感兴趣,我花了一周时间试图优化一个自定义的随机生成器---基于数学的,而不是基于系统调用的---让它足够快,比缓冲的 crypto/rand 还快。这不容易。你需要花很多心思才能让它比 crypto/rand 更快,尤其是如果你使用缓冲的 crypto/rand 读取器时。如果你有任何怀疑认为某种随机性可能会影响你服务的机密性、完整性或可用性,那就使用 crypto/rand 吧。它足够快。
Johan Brandhorst: 但我们提到为什么不总是使用 crypto/rand。我猜你在希望随机性可预测时会用 math/rand,对吧?
Filippo Valsorda: 当你希望随机性可复现时,当你希望你的测试每次都做相同的事情时,是的。如果你不希望两次不同的测试运行产生不同的结果,那么是的,你可以使用 math/rand。但我能想到的唯一例子是测试。也许其他人还能想到一些,但它们非常具体。
Roberto Clapis: [19:58] 即使是测试,也许你可以为你的随机性使用一个随机种子,然后在测试失败时记录种子……因为如果运行时是随机的,你希望测试尽可能接近现实。所以在测试中可以使用数学随机性,但每次使用不同的种子,这样如果有竞争条件,你就能看到它。
Mat Ryer: 哦,这是个非常有趣的观点。我从来没有想到过。当然,测试中的随机序列是可预测的,这是显而易见的自然方式。但你说得对,如果你测试的内容有这些随机元素,那么你当然希望它们能尽可能多地运行。
Filippo Valsorda: 是的。只要你记录下种子,这样你就不必运行一百万次来复现它,没错。我当然不是在说自己的经历,绝对不是。 [笑声]
Roberto Clapis: 是的。关于这个,我今天想谈一件事……那就是 go-fuzz。我不知道有多少人知道这个工具,但我发现它确实极大地提高了我的代码安全性和质量。对于那些还不了解的人,go-fuzz 是一个工具,它允许你以不同的方式编译代码,你只需要实现一个 Fuzz 函数,该函数接受一个字节切片并返回一个整数。如果你正确实现它,go-fuzz 会为你的测试增加很多价值,因为它会尝试伪随机输入并尝试探索你的所有代码。它检查哪些代码被执行了,哪些没有,然后不断随机化,直到它很好地覆盖你的代码……你会惊讶地发现,在我非常信任的代码中通过一个简单的 go-fuzz 函数发现了多少 bug。写这个函数只需要几分钟。
Filippo Valsorda: Go-fuzz 太棒了。OSS-Fuzz 的团队现在正在对标准库的一些 fuzzers 进行持续运行,这些 fuzzers 是由……
Roberto Clapis: Dmitry……
Filippo Valsorda: Dmitry Vyuokov,对吧?
Roberto Clapis: Dmitry。
Filippo Valsorda: 是的,我可能发音不对,抱歉……它在我们发布 Go 1.13 之前发现了 JSON 解码器的一个 bug,这让我避免了整个安全发布过程的麻烦,并且在进入生产之前就阻止了问题。Go-fuzz 真是太棒了。你不仅可以用它来发现崩溃和类似的问题,它自己会找到这些问题。但你还可以用它来强制执行不变量。例如,如果你正在使用缓冲区做些事情,你可以在调用某个解码器之前对缓冲区进行随机化,并确保旧的缓冲区不会影响新的结果。任何数量的不变量;你可以在 Fuzz 函数中编写的任何“这应该永远成立,若不成立则触发崩溃”的代码,go-fuzz 可以帮助探索,直到找到你意想不到的情况。
Johan Brandhorst: 我们是否应该退一步,稍微解释一下什么是 fuzzing,给那些可能不熟悉的人?我们刚讨论了 go-fuzz,这是一个 Go 特定的 fuzzing 包,但 fuzzing 对用户来说意味着什么呢?例如,如果你有一个处理用户输入的函数,你可能会考虑用户可能会输入什么,并尝试防止闭合括号之类的东西……但你可能没有意识到的是,实际上有工具可以处理你想不到的情况,或者你很难在测试中生成的情况,这些情况可能会导致你的应用崩溃。
所以 fuzzing 是一种自动发现会导致应用程序意外行为的问题字符串或字节序列的方法……它不仅通过随机数据来测试,还会给你的代码做插桩(instrument),看看“哦,如果我给它这些字节,它会进入这个分支。也许我接下来会尝试这个字节序列……”所以它是一个非常强大的工具,用来确保那些期望任意输入的函数不会崩溃或表现异常……
[24:01] 这也是黑客会使用的一种工具……许多早期开发的应用程序可能并没有经过 fuzzing 测试。如果你有一个没有速率限制的 API,你可以确定黑客会尝试 fuzz 它,寻找意外行为,甚至可能找到远程代码执行的漏洞。
Filippo Valsorda: 是的。举几个常见的 fuzzing 例子……例如 JSON 的例子---我们会在每次 fuzzing 迭代中随机生成一个字符串,并把它传递给 json.decoder,看看解码器是否做了我们不期望的事情。它发现了一个崩溃,因为它会进行数百万次尝试,并学习哪些东西会触发某些代码路径。它会像 Go 工具的 go test -cover 那样重写代码。这样它就找到了路径,正如 Johan 所说的。
让我感到惊讶的是---你说得对,这曾经是黑客编写的东西……而我从来没有真正理解我们是怎么走到这一步的。你能想象一个这样的世界吗?我们说“是的,编写单元测试确实是个好技巧。不幸的是,出于某种原因,只有安全研究人员会为别人的软件编写单元测试,仅仅是为了发现问题,然后测试完后就把它们扔掉。”但 fuzzing 就是这样---安全研究人员 fuzz 东西,报告他们发现的问题,然后他们转向别的项目……而 fuzzing 测试应该和单元测试、集成测试在同一个地方;它们应该由应用程序开发者自己编写。
Johan Brandhorst: 甚至有讨论要把这个功能集成到标准库中,使 fuzzing 成为测试工具的一等公民。
Roberto Clapis: 是的。如果我可以再补充一点非常简短的建议,那就是每当你的 fuzzer 找到一个导致程序崩溃的字符串时,立刻把它加入单元测试中。
Filippo Valsorda: 我其实是有意引导话题的,因为我真的希望模糊测试(fuzzing)能成为标准库的一部分。这个方面已经有了一些进展,我也在思考它应该是什么样子,并努力找到时间或是对这个感兴趣的人来推动这个事情……因为如果我们能有一个像 func fuzzFoo 这样的函数就太棒了,目前它接收一个字节切片,也许以后可以接收任何我们可以随机化的类型,然后它就像 go test 一样运行,也能做基准测试。
Mat Ryer: 你提到了 JSON 包,对我来说这简直是模糊测试的完美用例,因为它字面上就是在反序列化字符串。但如果你只是有一个函数,它只是用来生成一个问候语,比如说“你好,Filippo”,你只接收一个字符串作为名字。你会对这样的函数进行模糊测试吗?
Filippo Valsorda: 这里的回报和你投入的精力是成正比的。就像你不会为那个函数写一堆测试一样,对吧?你可能只会写一个简单的测试,而不会测试一堆边界情况。让我更感兴趣的是那些接收复杂输入的函数,但这些输入不是字节切片的形式。这是一个难题,因为你要如何随机化这些输入?你如何管理这些输入的样本库?当结构体里有了一个新字段,你该怎么办?你把所有的样本库都丢掉吗?那感觉很愚蠢。你应该尝试用已有的样本库,只是给新字段不同的值,但这非常困难。
Roberto Clapis: 让我来补充一些知识。样本库 基本上是一个目录,里面存放了 go-fuzz 认为有用的所有文件和输入,它会重用这些输入来生成更多的测试数据。
Mat Ryer: 它还会记住…
Roberto Clapis: 是的,是的。或者你可以随时中断它,它会从中断的地方继续。让我觉得特别难模糊测试的是 web 应用。如果回到我的领域,模糊测试一个应用并说“这里有一个 XSS”或“这里有一个跨站请求伪造”是很难的。我们在这方面有所进展,但还没有完全解决……特别是 XSS,因为跨站请求伪造可以通过其他方式解决,但 XSS 的模糊测试会很有用。
Mat Ryer: [28:23] 是的。你提到 web 应用很有趣,因为 Go 的一个吸引力在于启动一个 web 服务器非常简单,只需要 HTTP 监听和服务,如果你使用默认的模拟器和其他工具,你可以免费获得很多功能……感觉这已经足够了。但我们还需要做些什么来确保我们的服务器是安全的呢?
Roberto Clapis: 如果你使用默认设置并启动你的 web 服务,你会遇到一系列问题。我认为你可以运行一个 HTML 模板,这是可以的。但我见过有人像 fmt.Printf 那样将错误直接输出到 http.ResponseWriter,这是不好的。或者,如果你监听 POST 请求或表单提交,你就暴露在跨站请求伪造的风险中,而 Go 并不会警告你这些问题,因为 Go 的 HTTP 实现只是一个实现,它不是一个框架。
例如,如果你在服务中安装了 pprof 监听器,它会默认设置在 MUX 上,而你不希望将 pprof 暴露给互联网。所以在使用 MUX 的默认设置时会有很多问题,比如保持连接打开;有人可能会连接到你的服务六千次并将其拖垮。
Filippo Valsorda: 是的,超时是我的一个痛点。遗憾的是,根据 Go 1 的兼容性承诺,我们不能更改它们……因为如果我们为请求添加超时,那些比如流式传输超过一小时的请求就会中断,而我们不想破坏现有的功能。
Roberto Clapis: WebSockets。
Filippo Valsorda: 我的意思是,我们会对劫持的连接做特殊处理,但依然如此……所以当你使用默认 HTTP 服务器或默认客户端时,另一方可能会保持连接永远打开,你会泄露一个 goroutine 和一个文件描述符,最终你会耗尽文件描述符,然后在你出差中国时被通知问题出现(我绝对不是从自己的经历中说的)。
Mat Ryer: [笑] 那你会建议永远不要使用默认的功能吗?
Roberto Clapis: 是的。
Filippo Valsorda: 是的。你不应该使用 http.Get 这种辅助函数。你应该启动自己的客户端,在 HTTP 客户端的超时字段中设置超时,然后使用它……服务器也是如此。
Johan Brandhorst: 我记得有人几年前写过 一篇关于如何在网上保护你的 web 服务器的博客文章。那篇文章有持续更新吗?
Filippo Valsorda: [笑] 有人也有一个待办事项是更新那篇博客文章……[笑]
Roberto Clapis: 是的,特别是关于加密套件的部分,更新会很棒。
Filippo Valsorda: 是的……现在那篇文章可能有些过时了,不是吗……?
Roberto Clapis: 嗯嗯…… [笑]
Johan Brandhorst: 让我澄清一下……Filippo 在 Cloudflare 时写过一篇关于如何保护 Go web 服务器的博客文章---基本上是关于如何将 Go web 服务器暴露在互联网上……如果你想启动一个 web 服务器并将其暴露给互联网,那篇文章提供了一些不错的默认设置,值得去查看……但听起来未来可能会有更多内容添加。
Filippo Valsorda: Rob,你想把那篇文章变成一个 Wiki 页面吗?
Roberto Clapis: 哦……不,不。你不能让我自愿做这种事。
Filippo Valsorda: 我可以开始写……我只需要有人负责 web 部分。
Roberto Clapis: 哦,是的,我完全可以负责 web 部分……比如说“不要在脚本文件中插入任何内容”,或者“不接受任意请求”等等……而你可以处理那些繁重的工作,比如加密套件、默认设置之类的。
Filippo Valsorda: [32:07] 完美。是的,我们已经开始设置一些默认的响应头……这些响应头确保请求不会被误解为除了文本以外的其他东西。Rob,帮帮我。
Roberto Clapis: 我不太清楚这一点,但我知道我们仍然在为响应嗅探内容。
Filippo Valsorda: 是的,我们是在服务器端这样做的。我们真的无法修复这个问题。
Roberto Clapis: 对于那些不知道的人来说,内容类型是指服务器在向客户端发送响应时会告诉它“这是文本”或者“这是 JSON”或“这是一个二进制数据块”,而且重要的是,攻击者不能控制这一点,服务器也不能错误地告诉客户端“这是 HTML”,而实际上它是纯文本。
Filippo Valsorda: 是的,一个经典的攻击是你上传一张图片到一个允许上传图片的论坛,但实际上那是 HTML,然后某人用浏览器打开它,它运行某些 JavaScript,结果是---我不知道,也许你在那个论坛上得到了很多积分。
Roberto Clapis: 是的,问题在于 Go 会尝试猜测---你知道你刚才提到的简化过程,Mat?当你说“是的,简化过程很好,你只需启动它,它就会工作”时,是的。但它的工作方式是它为你做了一些它不应该做的事情。我在写 Go web 服务时做的一件事是将内容类型头设置为纯文本,即 text/plain。字符集设置为 UTF-8,仅此而已。我确信如果我忘记在 HTML 响应中设置内容类型,那么这些 HTML 就不会呈现。所以我从默认状态下是安全的。而当我确实需要提供 HTML 时,我会将内容类型重新设置为我知道的 HTML 类型。
Filippo Valsorda: 所以这就是我们需要在 Wiki 上写的一点---总是明确设置你的内容类型。
Mat Ryer: 是的。这真的很有趣,因为我们谈论的很多东西并没有在教 Go 的时候被教到。通常的教学是“如果你想要获取一些结果,可以使用 http.Get”。但它没有设置超时。我们往往就是这样学会这些基础工具的……但听起来在你将其用于生产环境之前,确实还有更多需要学习的东西。
使用 App Engine 的好处之一---我几乎只用它---是它会代替你处理很多安全层,我觉得在 App Engine 中你可以安全地使用 listen and serve,因为一切都通过代理。但你提到的其他一些问题绝对是适用的……而且可能适用于所有地方。
Filippo Valsorda: 我认为 Go 在这方面仍然比其他平台要好……只是 Go 已经有十年历史了,并且在这十年中它没有机会做出重大更改。所以任何我们没有在最初就正确实现的安全默认设置---这些一直被认为很重要---不幸的是,我们现在无法轻易改变。
Mat Ryer: 那你认为 Go 2 呢---如果有一天 Go 发布了一个重大版本,你是否有一份想要修正的清单?
Filippo Valsorda: 我不被允许谈论 Go 2。 [笑声] 不,我开玩笑的……Go 2 正在逐渐形成一个过程,通过这个过程我们可以做出重大更改,但并不会像 Python 2 到 Python 3 那样进行彻底的转变。我们已经在称 Go 1.13 中的语言更改为 Go 2……我猜想总有一天我们会想要为标准库中的一些包制作 v2 版本,但我们还没有这个基础设施,也不知道如何去做。也许最终会有一个 net/http/v2,就像你可以有 modules/v2 一样。
Roberto Clapis: 对于安全性,我有一个想法---你知道,在 web 平台上我们不能真正弃用某些功能,因为 web 平台已经在那里了,如果一个浏览器开始破坏网站,人们会转而使用其他浏览器……所以没有浏览器会完全移除一个功能。我们需要某些功能继续存在一段时间。所以我们通常通过某种版本控制来解决这个问题。服务端会向浏览器添加一个头部,告诉浏览器“我想要这个安全级别,并禁用任何会降低此安全级别的功能。”
[36:22] 我计划为我们已经讨论的 HTML 模板库做的一件事是,当你解析模板时,你希望它是安全的。这将改变你的 HTML 以防止某些漏洞。我们不能让这成为默认行为,因为那会是一个破坏性更改,但如果我们添加一个更好的 API,并告诉大家“嘿,使用 .secure 并从现在开始传递一个级别”,每次我们升级时,我们可以提高这个级别并继续前进。现在这听起来很空洞,但即使在 Go 2 之前,我们也能通过这种方式获得一些默认的安全性。
Filippo Valsorda: 是的,从某种程度上说,告诉人们“哦,你没调用 http.secure,所以才有问题”是非常痛苦的。
Mat Ryer: 这很有趣。
Filippo Valsorda: [笑]
Mat Ryer: 这让我想起加密和 math/rand 的事情。它就像“你没有使用安全的那个。”“那么为什么你要创造一个不安全的呢?什么是安全的六?我从这个函数得到的是六,但我想要的是安全的六。”
Roberto Clapis: 是的,但毕竟,你会用竞态检测器(race detector)运行你的生产服务器吗?对某些事情来说,这确实有点道理。也许对于安全性来说,调用 http.secure 并没有意义,但对于某些事情,我觉得我们会有一个更安全的版本,在发生不好的事情时会发出警告,比如 -race 标志……你可以运行一段时间,以便给你的代码加上检测,如果发生了不好的事情,然后你就可以专注于性能。所以我觉得有些东西可能会一直存在。
Mat Ryer: 当然,你还可以有静态分析工具或 lint 工具来帮助你。实际上,我看到有人在做一个项目,是一家名为 ShiftLeft 的公司;他们基本上是在做安全方面的静态分析工具。一个示例是,如果有一个名为 “password” 的字符串,并且它被打印出来了,你会收到警告。我们今天应该使用的其他工具有哪些?你还能想象出其他什么工具吗?
Roberto Clapis: 是的。最棒的工具之一就是类型系统。当你有了类型 password,你可以将字符串包装在一个不透明的结构体中,并实现 Stringer 接口,而 Stringer 接口会打印星号。
Johan Brandhorst: 你觉得 Roberto 喜欢这种模式吗? [笑声]
Roberto Clapis: 如果编译通过了,那应该就是安全的。
Mat Ryer: 那么 password 就变成了一个不可打印的类型,因为我们知道所有的打印---
Roberto Clapis: 不,它是可打印的。它会打印星号。
Johan Brandhorst: 它只会打印 [无法听清 00:39:00.17]
Filippo Valsorda: Rob,答应我你永远不要去研究 Rust。我们需要你继续做 Go。
Roberto Clapis: 我会的。我会的。
Johan Brandhorst: 你没看他的推特吗?
Roberto Clapis: 我花了很多时间在 Rust 上,所以我是这个想法的忠实粉丝。但它并不真的吸引我。
Filippo Valsorda: 是的,我开这个玩笑的原因是 Rust 在复杂性和类型系统的强大之间选择了不同的平衡点,所以你可以做很多类似的事情,但这也意味着代码库可能会变得更加复杂。
Mat Ryer: 当然,这会引入潜在的安全问题,就像我们之前学到的一样。
Filippo Valsorda: 是的,这是一个权衡。
Mat Ryer: 是的,一个权衡。
Roberto Clapis: 我现在正在给 Mat 发送心形表情符号。 [笑声]
Mat Ryer: 谢谢你,我收到了,非常感动。
Roberto Clapis: 如果我们想改变一下话题,我真的很想讨论一下依赖库的问题。
Filippo Valsorda: 呃…
Mat Ryer: 继续,你指的是什么?
Roberto Clapis: [40:00] 你知道的,选择一个依赖库的过程并不简单,比如“我该如何选择一个想依赖但又不想重新实现的库?” 尤其是从安全角度来说,你想要保护自己免受 CSRF(跨站请求伪造)攻击,而这并不在标准库中。你该如何应对这种情况?这个问题真的很复杂。Johan,你有什么建议吗?
Johan Brandhorst: 显然,你需要将代码放入项目中并在添加之前审查所有代码……不,我在开玩笑。这完全不现实。 [笑]
Roberto Clapis: 是的,那是理想状态……但有什么可行的方案吗?
Johan Brandhorst: 哇……你把这个问题扔给我了。我觉得目前还没有一个特别好的答案。我知道 Go 现在有一个新的 Discover 网站……也许能稍微帮助你找到更有信誉的包,尽管我不确定它是否专注于安全……但这些库通常有好的维护者,会响应问题,并合并拉取请求。显然,这是个很难的问题。
Filippo Valsorda: Discover 网站的作者 Julie 做过一个完整的关于寻找可靠依赖库的演讲。我认为她的讲座已经涵盖了我想说的大部分内容……在安全方面,我们可能需要一种方式让作者标记安全问题,或者任何方式来标记元数据,这样我们就可以在 Discover 网站上展示出来。
Discover 网站还可以做其他事情,比如对使用已弃用 API 或库的用户发出警告,这也是我最喜欢的解决方法之一。当我无法删除某些东西时,我可以弃用它,并希望每个人都在使用静态检查工具,这样他们就会收到警告。但这又是一个需要我找到时间或找到愿意为这个生态系统工作的人才能解决的难题……找到标记安全问题的方法。而这是一个难题,因为如果模块没有维护该怎么办?你如何展示已报告的问题?你是否会认为未由作者发布的报告是有效的?这些是不同生态系统有不同答案的问题。
插播内容: [42:25]
Mat Ryer: 另外,你会在多长时间内公开这些信息?有时你可能会发现一个漏洞,但你不希望别人知道。
Roberto Clapis: 是的,Go 增加了更多的复杂性,因为 Go 是静态链接的。假设有人发布了一个 Go 模块,你确保所有的发行版都导入了这个 Go 模块,并重新编译以修复安全问题,而他们只是使用了这个修复版……这很好,但编译好的二进制文件呢?你如何检查某个 Go 二进制文件是否使用了仍然存在漏洞的之前版本库?
想想一个 Linux 发行版。你不想重新推送所有依赖某个库的二进制文件。也许你想修复某个安全问题,但……
Filippo Valsorda: 我们确实希望他们采取这种方式,而不是试图让动态链接生效……
Roberto Clapis: [44:07] 是的,这仍然比试图在其中插入一些糟糕的黑客解决方案要好。但如你所见,这确实是个问题。而且,当你为某个库发布补丁时,你需要公开这个补丁,而有些黑客会积极寻找这些补丁,分析它们是否涉及安全问题,如果是,他们会在其他人修复之前,尽可能地利用所有使用该库的系统。
Filippo Valsorda: 一个好的元数据传播生态系统可以帮助解决这个问题。你可以拥有一些工具来查看二进制文件,从 Go 1.13 版本开始,其中包含了构建时所用的所有模块的版本信息。在 debug.BuildInfo 中包含了所有模块的名称和版本。
Mat Ryer: 这些信息是和构建一起生成的吗?
Filippo Valsorda: 是的,它们在二进制文件中。
Mat Ryer: 哦,是嵌入在二进制文件里的吗?
Filippo Valsorda: 是的。而且 Go 1.13 中有一个新功能,如果你输入 go version binary.foo,它会告诉你该二进制文件的所有构建信息。所以你可以运行 go version bla 并获取 Go 版本、模块版本的列表……如果我们有办法发布关于不同版本的结构化元数据,并指出哪些版本存在什么问题,我们就可以有自动化系统来检查你生产环境中的二进制文件,并提示“等等……这个二进制文件是使用已知不安全的版本构建的。”问题在于如何定义“已知的不安全”。
Roberto Clapis: 是的,简单吧。
Filippo Valsorda: 对,我们只需要解决这个问题。 [笑声]
Mat Ryer: 我刚意识到,这期播客对任何想成为黑客的人来说都会非常有帮助。 [笑声]
Filippo Valsorda: 你是说我们给出了错误的建议?
Mat Ryer: 嗯,只不过扩大了听众群体……现在我知道他们使用 JIRA 之类的工具,祝他们好运吧。
Filippo Valsorda: 但是,关于披露时间线的讨论,现在业界越来越普遍接受,长时间的保密并没有多大帮助。如今的标准是 90 天,如果需要发布补丁,可以再延长 15 天……因为到了某个时间点,防御者需要知道,而攻击者也会重新发现这些问题。
我正在处理一个安全问题,我尽量不剧透,因为那会非常尴尬……但这个问题在两周内通过两个不同的 security@golang.org 报告被提交了。同样,攻击者也能找到这些问题。虽然防御者没有时间或预算去检查所有事情,但攻击者却在积极寻找漏洞。
Roberto Clapis: 如果有人在听,并且觉得自己发现了一个安全漏洞,请通过电子邮件报告,不要公开提交问题。
Filippo Valsorda: 是的,请这样做。
Roberto Clapis: 是的。
Mat Ryer: 那电子邮件地址是什么?
Filippo Valsorda: 是 security@golang.org。
Mat Ryer: 好的。
Roberto Clapis: 这基本上就是 Filippo 和其他几个人,所以……你知道你在跟谁说话。
Filippo Valsorda: 你很可能会收到我的回复,是的。但如果我在度假,还有其他人会顶替我。Security@golang.org。我们的呼叫中心在等待您的来电! [笑声]
Johan Brandhorst: 您的来电对我们很重要!
Roberto Clapis: 您在队列中排名第 741 位,请稍等。
Johan Brandhorst: [笑声] 希望不是这样。
Mat Ryer: 为完成提交,请输入您母亲的姓氏。
Roberto Clapis: [笑]
Filippo Valsorda: 这是唯一一个如果你发了好东西,我们可能会给你钱的电子邮件地址。
Mat Ryer: 哦,有趣。
Roberto Clapis: 哦,这是 VRP(漏洞奖励计划)的一部分吗?我不知道这一点。
Filippo Valsorda: 我尽量不大肆宣传这件事,因为我不想让报告数量过多,导致漏洞奖励计划充满噪音……但每当我们收到特别好或特别有趣的报告时,我会推荐他们去领取漏洞奖励计划的奖金。
Roberto Clapis: 嗯,好的。你知道,如果漏洞报告太多,我们有专门的团队来处理这些问题。 [笑声]
Mat Ryer: [48:11] 你现在会收到很多这样的报告了。你会得到各种各样的东西,比如---好吧,我不想帮忙。 [笑声] 那么你听过的最疯狂的安全相关故事是什么?或者你自己遇到过的?
Roberto Clapis: 跟 Go 相关的吗?
Mat Ryer: 不一定。
Johan Brandhorst: 你有很多可以选择的,是吗?
Roberto Clapis: [笑]
Mat Ryer: 你是在犹豫要不要讲这个故事吗?
Roberto Clapis: 是的……
Mat Ryer: 因为我可以帮你,讲吧!
Johan Brandhorst: 是的,讲吧。
Roberto Clapis: 有一些简单的事情,我发现人们并不在意,或者根本没有考虑到。前几天我在审查 x/net 中的 XSRF 令牌包时,发现它会用其他字符替换某些字符,以确保某个字符串分割能成功……这意味着用户可以通过精心构造他们的用户名,导致他们的 CSRF 令牌与另一个用户的令牌相同。
基本上,他们通过懒惰的方式导致安全令牌冲突,因为他们没有做正确的转义处理……我觉得编程者的懒惰是导致安全问题的主要原因。这是一个明显的例子。
Filippo Valsorda: 是啊,顺便说一句,感谢你。我当时在特内里费岛度假,正好处理你的报告……真是太棒了。 [笑]
Roberto Clapis: 是啊……当我在标准库中发现 DNS 重绑定问题时,我被告知“不要公开提交问题,写信给 security@。”
Filippo Valsorda: 我只是开玩笑的,你做得很对。 [笑声]
Mat Ryer: 你有收到奖金吗,Roberto?
Roberto Clapis: 没有。为什么我要……
Filippo Valsorda: 你在 Google 工作,Google 不会给你发奖金。
Roberto Clapis: 其实,我可以用奖金来资助我的团队。你知道有些团队通过发现其他团队的漏洞来获得资助吗?
Mat Ryer: 哦,真的吗?
Roberto Clapis: 是的。我不知道这是否属实,可能是个传说,但……为什么不呢?
Mat Ryer: 你提到了程序员的懒惰……这是一个有趣的观点,但---实际上,很多团队都在赶进度;他们快速开发软件,面临很大压力,很多人觉得这就是开发软件的方式。但也有人提出了一个很好的论点,认为我们应该放慢脚步,多花一点时间,这样我们也许可以避免一些问题。
Roberto Clapis: 是的。有一句话我们经常说:“乐观者很快部署并快速工作,然后写故障报告。” [笑声] 悲观者写测试和模糊测试函数,他们可以安稳地睡觉。
Mat Ryer: 是的,还有其他疯狂的故事吗?
Filippo Valsorda: 我仍然不打算责怪开发者懒惰。虽然这确实是漏洞的来源之一,但很多时候我们提供的是不安全的平台、不安全的默认设置、不安全的架构。我敢肯定,我们曾经把很多由于像 C 语言中的字符串
Filippo Valsorda: 我仍然不喜欢批评开发者懒惰。的确,懒惰通常是漏洞的来源,但很多次其实是因为我们提供了不安全的平台、不安全的默认设置、不安全的架构。我确信我们曾经责怪开发者犯下了很多漏洞,比如在 C 语言中用字符串复制造成的漏洞。但现在我们知道,这就像责怪那些不断触碰暴露的高压电线的人。别碰那根电线就行了![笑声]
Roberto Clapis: 是啊。如果所有程序员都用错了某个 API,那问题就不在程序员身上。
Filippo Valsorda: 对。但你知道,电线旁边还会有个小标签,详细说明了电压和电流强度,如果你读懂了并且了解这些电压和电流,你就知道会有生命危险。他们应该读读标签。
Mat Ryer: [52:03] 读标签。永远读标签,没错。
Filippo Valsorda: 当然,我这是在讽刺。不过是的…… [笑] 说到疯狂的故事……我不太擅长回答这种问题。但有一个故事一直让我记忆深刻,我怀疑这可能就是我进入安全领域的原因。当时在 Freenode 上的 Wikipedia 频道里有一个 IRC 机器人,那时 IRC 还很流行---
Roberto Clapis: 哇,这是哪一年的事了?
Filippo Valsorda: 我知道,对吧?!那个机器人根据你是谁来执行一些操作。在 IRC 上你可以更改昵称,而这个机器人会根据你的身份允许你做一些事情;如果你是频道管理员之一,机器人就会给你管理员权限,或者让你把别人踢出去,或者进行“踢封”(kick-ban)操作……踢封真的很有趣。不过为了确保这个昵称不是某人在管理员离线时冒名顶替的,机器人会向服务发送一条消息,询问这个人的身份,然后服务会返回一条响应。如果这个人是经过身份验证的,登录时输入了密码,它就会说“Filippo 已通过服务认证”。所以你可以说:“嘿,把这个人踢出去,我是管理员。”然后机器人会说:“嘿,这个人是管理员吗?”接着它会收到响应:“哦,是的,他已通过服务认证。”然后机器人就会把人踢出去并封禁。
不过,你可以自己在频道里说“已通过服务认证”,机器人就会相信你。所以你可以把昵称改成管理员的名字,说“踢封”,机器人可能会说“不行”,然后你再说“管理员已通过服务认证。”“好的。踢。封禁。搞定。”
Mat Ryer: 哇,这个机器人真可爱。真是个愚蠢的机器人。 [笑声] 不如直接弄个选项框,写上“请保证不要做坏事,好吗”,然后打勾就行了。
Filippo Valsorda: 但这正是 Rob 刚才说的,关于输入信号被认为是指令的问题。
Roberto Clapis: 是的,每次你看到一个非常严重的漏洞,通常都是数据和代码混在一起造成的。
Mat Ryer: 哦,看来我们发现了一个模式……
Roberto Clapis: 没错。
Mat Ryer: 好吧,今天我们经历了一次漫长的旅程,从模糊测试到---你知道的,Go 语言的合理默认设置。Filippo 还提到,现在我们要把这些合理默认扩展到安全默认,并确保默认情况下就是安全的。这需要付出很多努力,也很困难……我相信我们会继续讨论这个话题很长时间。
感谢我们的嘉宾,Roberto Clapis、Johan Brandhorst 和 Filippo Valsorda。非常感谢!我们下周见。
原文链接: https://dashen.tech/2018/09/21/Security-for-Gophers/
版权声明: 转载请注明出处.