Git 的 --end-of-options 标志
Hacker News 摘要原标题:git's –end-of-options Flag
Andrew Nesbitt 在这篇文章中详细探讨了 Git 的一个冷门标志 --end-of-options 及其在防止参数注入漏洞中的重要作用。
Git 参数解析的歧义问题
在大多数 Unix 工具中,-- 符号标志着选项解析的结束。例如,执行 rm -- -f 会删除名为 -f 的文件,而不会将其误认为是强制删除的参数。
然而,Git 早就将 -- 挪作他用,主要用于区分修订版本和路径规范。比如在 git log main -- README.md 中,-- 明确表示后面的是文件路径。这导致 Git 的修订版本位置缺乏终止符。如果脚本运行 git log "$rev",且变量 $rev 以连字符开头,Git 就会将其解析为一个功能选项而非版本号。
--end-of-options 标志的由来
为了解决上述歧义,Git 在 2019 年 11 月发布的 2.24.0 版本中引入了 --end-of-options。它的作用是明确告知 Git,此后的所有参数都不应再被视为选项或标志。
需要注意的是,-- 和 --end-of-options 在 Git 中功能不同,不能混用。对于需要安全处理不受信任输入的命令,正确的写法应当是 git log --end-of-options "$rev" -- "$path"。这两个标记各司其职,前者保护修订版本,后者保护路径。
该标志的支持是逐步完善的:
• git rev-parse 直到 2.30.0 版本才支持。
• git checkout 和 git reset 直到 2.43.1 版本(2024 年 2 月)才支持,因为这些命令内部有自定义的参数解析器。
参数注入攻击(CWE-88)
Git、Mercurial 和 SSH 都提供了一些允许运行指定命令的选项。例如,git clone 支持 --upload-pack 选项来指定服务器端二进制文件。
当包装程序将不受信任的字符串传递给这些工具时,就会发生参数注入攻击。这与命令注入不同,因为它不涉及 Shell 解释,而是利用了程序将以连字符开头的普通参数误认为功能标志的特性。历史上已发生多起此类漏洞:
• CVE-2019-13139:Docker 构建过程中的漏洞。
• CVE-2017-1000117:涉及 Git 等多个版本控制系统的协同披露漏洞。
包管理器的安全现状
大多数语言的包管理器都会通过派生 Git 进程来处理依赖项。在作者调研的 19 个包管理器中,有 17 个默认使用 Git 二进制文件。
历史上,Bundler、Composer、Poetry、pip 和 Go 等包管理器都曾遭遇过此类参数注入漏洞。目前,只有 Go 语言的 cmd/go 广泛使用了 --end-of-options 标志。
其他工具之所以未广泛采用,主要原因包括:
• 兼容性问题:许多工具需要支持旧版本的 Git。例如 Composer 选择了拒绝以连字符开头的分支名,而不是使用该标志,因为其用户可能仍在使用不支持该标志的老旧 Git 环境。
• 系统环境落后:一些长期支持版的 Linux 发行版(如 Ubuntu 18.04 或 20.04)自带的 Git 版本可能完全不支持该标志,或者只在部分子命令中支持。
解决方案与建议
为了彻底根除此类漏洞,开发者可以采取以下措施:
• 使用原生库:如 libgit2、gitoxide 或 JGit。由于这些库在进程内实现协议,不涉及命令行调用,因此不存在参数注入的边界。但缺点是需要同步上游 Git 的安全补丁。
• 提升最低版本要求:作者已向 Homebrew 提交请求,建议将其要求的最低 Git 版本提升至 2.30.0,并在相关命令中添加 --end-of-options 保护。
• 谨慎解析输入:在无法升级 Git 环境时,应对所有来自外部的引用和 URL 进行严格校验,禁止以连字符开头的非法输入。