:::info
日期:2021 年 01 月 19 日
作者:Russ Cox
原文链接:https://go.dev/blog/path-security
:::
今天的 Go 安全版本修复了一个涉及在不受信任的目录中查找 PATH 的问题,该问题可能导致在 go get 命令期间远程执行。 我们希望人们对这究竟意味着什么以及他们自己的程序是否可能存在问题有疑问。 这篇文章详细介绍了错误、我们应用的修复程序、如何确定您自己的程序是否容易受到类似问题的影响,以及如果出现问题您可以做什么。
Go 命令与远程执行
go 命令的设计目标之一是大多数命令(包括 go build、go doc、go get、go install 和 go list)都不会运行从 Internet 下载的任意代码。 有一些明显的例外:显然 go run、go test 和 go generate 运行任意代码——这是他们的工作。 但其他人不能,出于各种原因,包括可复制的构建和安全性。 所以当 go get 可以被欺骗执行任意代码时,我们认为这是一个安全漏洞。
如果 go get 不能运行任意代码,那么不幸的是,这意味着它调用的所有程序,例如编译器和版本控制系统,也在安全范围内。 例如,我们过去曾遇到过这样的问题,即巧妙地使用晦涩的编译器功能或版本控制系统中的远程执行错误在 Go 中变成了远程执行错误。 (在这一点上,Go 1.16 旨在通过引入 GOVCS 设置来改善这种情况,该设置允许准确配置允许哪些版本控制系统以及何时允许。)
然而,今天的错误完全是我们的错,而不是一个 bug 或者 gcc 晦涩的功能或 git 的错误。 该错误涉及 Go 和其他程序如何找到其他可执行文件,因此我们需要花一点时间查看它,然后才能了解详细信息。
命令、PATH 和 Go
所有操作系统都有一个可执行路径的概念(Unix 上为 $PATH,Windows 上为 %PATH%;为简单起见,我们将只使用术语 PATH),它是一个目录列表。 当您在 shell 提示符中键入命令时,shell 会依次在每个列出的目录中查找具有您键入的名称的可执行文件。 它运行它找到的第一个,或者打印一条消息,如“未找到命令”。
在 Unix 上,这个想法首先出现在第七版 Unix 的 Bourne shell (1979) 中。 手册解释说:
shell 参数 $PATH 定义包含命令的目录的搜索路径。 每个备用目录名称由冒号 (:) 分隔。 默认路径是:/bin:/usr/bin。 如果命令名称包含 /,则不使用搜索路径。 否则,将在路径中的每个目录中搜索一个可执行文件。
请注意默认值:当前目录(此处由空字符串表示,但我们称其为“点”)列在 /bin 和 /usr/bin 之前。 MS-DOS 和 Windows 选择硬编码该行为:在这些系统上,在考虑 %PATH% 中列出的任何目录之前,总是首先自动搜索点。
正如 Grampp 和 Morris 在他们的经典论文“UNIX 操作系统安全性”(1984 年)中指出的那样,在 PATH 中将点放在系统目录之前意味着如果您 cd 进入一个目录并运行 ls,您可能会从该目录中获得恶意副本而不是系统实用程序。 如果您可以欺骗系统管理员在以 root 身份登录时在您的主目录中运行 ls,那么您可以运行任何您想要的代码。 由于这个问题和其他类似的问题,基本上所有现代 Unix 发行版都设置了一个新用户的默认 PATH 以排除点。 但是 Windows 系统继续先搜索 dot,无论 PATH 说什么。
例如,当您键入命令时
go version
在典型配置的 Unix 上,shell 从 PATH 中的系统目录运行 go 可执行文件。 但是当您在 Windows 上键入该命令时,cmd.exe 首先检查点。 如果 .\go.exe(或 .\go.bat 或许多其他选项)存在,则 cmd.exe 运行该可执行文件,而不是来自 PATH 的可执行文件。
对于 Go,PATH 搜索由 exec.LookPath 处理,由 exec.Command 自动调用。 为了很好地适应主机系统,Go 的 exec.LookPath 在 Unix 上实现了 Unix 规则,在 Windows 上实现了 Windows 规则。 例如,这个命令
out, err := exec.Command("go", "version").CombinedOutput()
行为与在操作系统 shell 中键入 go version 相同。 在 Windows 上,它会在 .\go.exe 存在时运行。
(值得注意的是,Windows PowerShell 改变了这种行为,删除了 dot 的隐式搜索,但 cmd.exe 和 Windows C 库 SearchPath 函数继续像往常一样运行。Go 继续匹配 cmd.exe。)
Bug
当 go get 下载并构建一个包含 import “C” 的包时,它会运行一个名为 cgo 的程序来准备相关 C 代码的 Go 等价物。 go 命令在包含包源的目录中运行 cgo。 一旦 cgo 生成了它的 Go 输出文件,go 命令本身就会调用生成的 Go 文件上的 Go 编译器和主机 C 编译器(gcc 或 clang)来构建包中包含的任何 C 源。 所有这些都运作良好。 但是 go 命令在哪里找到宿主 C 编译器呢? 当然,它在 PATH 中查找。 幸运的是,当它在包源目录中运行 C 编译器时,它会从调用 go 命令的原始目录执行 PATH 查找:
cmd := exec.Command("gcc", "file.c")
cmd.Dir = "badpkg"
cmd.Run()
所以即使 badpkg\gcc.exe 存在于 Windows 系统上,这段代码片段也不会找到它。 在 exec.Command 中发生的查找不知道 badpkg 目录。
go 命令使用类似的代码来调用 cgo,在这种情况下甚至没有路径查找,因为 cgo 总是来自 GOROOT:
cmd := exec.Command(GOROOT+"/pkg/tool/"+GOOS_GOARCH+"/cgo", "file.go")
cmd.Dir = "badpkg"
cmd.Run()
这比之前的代码片段更安全:不可能运行任何可能存在的坏 cgo.exe。
但事实证明,cgo 本身也调用主机 C 编译器,在它创建的一些临时文件上,这意味着它自己执行以下代码:
// running in cgo in badpkg dir
cmd := exec.Command("gcc", "tmpfile.c")
cmd.Run()
现在,因为 cgo 本身在 badpkg 中运行,而不是在运行 go 命令的目录中,如果该文件存在,它将运行 badpkg\gcc.exe,而不是查找系统 gcc。
因此,攻击者可以创建一个使用 cgo 并包含 gcc.exe 的恶意包,然后任何运行 go get 的 Windows 用户下载并构建攻击者的包将优先运行攻击者提供的 gcc.exe,而不是运行中的任何 gcc。 系统路径。
Unix 系统首先避免了这个问题,因为点通常不在 PATH 中,其次是因为模块解包不会在它写入的文件上设置执行位。 但是,在 PATH 中系统目录前面有一个点并且使用 GOPATH 模式的 Unix 用户将与 Windows 用户一样容易受到影响。 (如果这描述了您,那么今天是从您的路径中删除 dot 并开始使用 Go 模块的好日子。)
补丁
go get命令下载并运行恶意gcc.exe显然是不能接受的。 但允许这样做的实际错误是什么? 然后解决方法是什么?
一个可能的答案是错误是 cgo 在不受信任的源目录中而不是在调用 go 命令的目录中搜索主机 C 编译器。 如果这是错误,那么修复方法是更改 go 命令以将完整路径传递给主机 C 编译器,以便 cgo 无需在不受信任的目录中执行 PATH 查找。
另一个可能的答案是错误是在 PATH 查找期间查找点,无论是在 Windows 上自动发生还是由于 Unix 系统上的显式 PATH 条目。 用户可能想在 dot 中查找他们在控制台或 shell 窗口中键入的命令,但他们不太可能也想在那里查找键入命令的子进程的子进程。 如果这是错误,那么修复方法是更改 cgo 命令,使其在 PATH 查找期间不查找点。
我们认为这两个都是错误,所以我们应用了两个修复程序。 go 命令现在将完整的主机 C 编译器路径传递给 cgo。 最重要的是,cgo、go 和 Go 发行版中的所有其他命令现在都使用 os/exec 包的变体,如果它以前使用过来自 dot 的可执行文件,则会报告错误。 包 go/build 和 go/import 使用相同的策略来调用 go 命令和其他工具。 这应该关闭可能潜伏的任何类似安全问题的大门。
出于谨慎考虑,我们还在 goimports 和 gopls 等命令以及库 golang.org/x/tools/go/analysis 和 golang.org/x/tools/go/packages 中进行了类似的修复,其中 将 go 命令作为子进程调用。 如果你在不受信任的目录中运行这些程序——例如,如果你 git checkout untrusted repositories 和 cd 进入它们,然后运行这样的程序,并且你使用 Windows 或在你的 PATH 中使用带有点的 Unix——那么你应该更新这些命令。 如果您计算机上唯一不受信任的目录是 go get 管理的模块缓存中的目录,那么您只需要新的 Go 版本。
更新到新的 Go 版本后,您可以使用以下命令更新到最新的 gopls:
GO111MODULE=on \
go get golang.org/x/tools/gopls@v0.6.4
您可以使用以下方法更新到最新的 goimports 或其他工具:
GO111MODULE=on \
go get golang.org/x/tools/cmd/goimports@v0.1.0
您可以通过在 go get 期间添加依赖项的显式升级来更新依赖于 golang.org/x/tools/go/packages 的程序,甚至在其作者这样做之前:
GO111MODULE=on \
go get example.com/cmd/thecmd golang.org/x/tools@v0.1.0
对于使用 go/build 的程序,使用更新的 Go 版本重新编译它们就足够了。
同样,如果您是 Windows 用户或 PATH 中带有点的 Unix 用户,并且您在不信任的可能包含恶意程序的源目录中运行这些程序,则您只需要更新这些其他程序。
你的项目受影响了吗?
如果您在自己的程序中使用 exec.LookPath 或 exec.Command,您只需担心您(或您的用户)是否在包含不受信任内容的目录中运行您的程序。 如果是这样,则可以使用 dot 中的可执行文件而不是系统目录来启动子进程。 (同样,使用来自 dot 的可执行文件总是在 Windows 上发生,并且只在 Unix 上使用不常见的 PATH 设置。)
如果你担心,那么我们已经发布了 os/exec 的更受限制的变体作为 golang.org/x/sys/execabs . 您可以通过简单地使用
import exec "golang.org/x/sys/execabs"
替代
import "os/exec"
后重新编译。
默认的 os/exec 安全性
我们一直在 golang.org/issue/38736 上讨论是否应该更改在 PATH 查找中(在 exec.Command 和 exec.LookPath 期间)始终首选当前目录的 Windows 行为。 支持这一改变的论点是它解决了这篇博文中讨论的各种安全问题。 一个支持的论点是,虽然 Windows SearchPath API 和 cmd.exe 仍然总是搜索当前目录,但 cmd.exe 的后继者 PowerShell 不会,这明显认识到原始行为是错误的。 反对更改的论点是它可能会破坏打算在当前目录中查找程序的现有 Windows 程序。 我们不知道有多少这样的程序存在,但是如果 PATH 查找开始完全跳过当前目录,它们就会出现无法解释的失败。
我们在 golang.org/x/sys/execabs 中采用的方法可能是一个合理的中间立场。 它找到旧的 PATH 查找结果,然后返回一个明确的错误,而不是使用当前目录的结果。 当 prog.exe 存在时,从 exec.Command(“prog”) 返回的错误如下所示:
prog resolves to executable in current directory (.\prog.exe)
对于确实改变行为的程序,这个错误应该很清楚发生了什么。 打算从当前目录运行程序的程序可以使用 exec.Command(“./prog”) 代替(该语法适用于所有系统,甚至 Windows)。
我们已将此想法作为新提案提交,golang.org/issue/43724。