```Git 的 --end-of-options 标志```
git's –end-of-options Flag

原始链接: https://nesbitt.io/2026/07/21/end-of-options.html

本文重点介绍了在通过子进程调用 `git` 的工具中存在的**参数注入**(CWE-88)风险。由于 Git 会将某些参数视为配置标志(例如 `--upload-pack`),攻击者即使在程序不使用 shell 的情况下,也能够通过提供以连字符开头的恶意字符串来重写 Git 的行为。 尽管 Git 引入了 `--end-of-options` 标志来安全地终止选项解析,但该标志经常被错误使用或忽视。标准的 `--` 分隔符通常是不够的,因为它们在 Git 复杂的命令行语法中承担着不同的角色。 许多包管理器(如 Bundler、Composer、pip)历史上都曾存在此类漏洞,且往往是在漏洞被利用后才发现问题。由于需要支持旧版 Linux 发行版中提供的老旧 Git 版本,修复工作变得十分复杂;使用 `--end-of-options` 需要较新的 Git 版本(2.24.0 或更高版本,某些子命令甚至需要更新的版本)。虽然一些开发者使用纯 Git 库(如 `libgit2`)来彻底避开参数解析问题,但这些库也带来了额外的维护负担。作者最后呼吁在生态系统中更一致地使用 `--end-of-options`,并提高对 Git 最低版本的版本要求。

这篇 Hacker News 讨论聚焦于对 Git 命令行界面的不满,特别是 `git` 命令在处理 `--`(选项结束符)标志时存在的不一致性。 用户批评 Git 的用户体验“非常奇特”且设计拙劣,感叹必须强行记忆特定工具的语法,而非遵循通用惯例。对话中强调了在区分分支名称、文件路径和命令标志时所产生的认知负担,尤其是当有用户提议通过将分支命名为 `--` 这种恶搞方式来揭示其荒谬性时。 有参与者指出,这些可用性问题源于缺乏“面向对象”的命令结构,并将 Git 的行为与 PowerShell 等系统进行了对比,后者通过类型化对象提供了更高的清晰度。归根结底,该讨论串批判了 Git 强大的数据模型正因其不直观且碎片化的用户体验而大打折扣。
相关文章

原文

I was reading through the fix for a package manager CVE last week and ran into a git flag I’d somehow never noticed: --end-of-options. My first reaction was that some LLM had hallucinated it, but it’s documented in gitcli(7), it was added in git 2.24.0 in November 2019, and it exists because git had already used -- for something else.

In most Unix tools -- marks the end of option parsing, so rm -- -f removes a file called -f rather than passing the force flag. Git had repurposed -- early on to separate revisions from pathspecs, because git log foo on its own is ambiguous between a branch named foo and a file named foo and one of the two readings needed a marker: git log main -- README.md means commits on main touching that file. That left the revision position with no terminator, so if a script runs git log "$rev" and $rev starts with a dash, git parses it as an option.

From the commit that introduced --end-of-options:

But that doesn’t work for the revision parser, because -- is already meaningful there: it separates revisions from pathspecs. So we need some other marker to separate options from revisions.

-- and --end-of-options are different things in git, and treating them as interchangeable is a mistake I’ve now seen in several places. Putting -- before a URL in git clone -- "$url" works, because clone follows the POSIX convention. A trailing -- after a ref, as in git checkout "$ref" --, marks $ref as a revision rather than a filename but still lets it be read as an option first. Passing an untrusted revision safely means writing git log --end-of-options "$rev" -- "$path", with both markers doing separate jobs.

Support for the new flag arrived per subcommand rather than all at once: git rev-parse only got it in 2.30.0, a year after the initial release, because it has its own hand-rolled argument parser, and git checkout and git reset rejected it until 2.43.1 in February 2024 because they parse -- themselves and the initial implementation left --end-of-options in the argument list where their parsers rejected it.

Argument injection

Git, hg, and ssh all ship options whose documented purpose is to run a command the caller names. git clone accepts --upload-pack=<cmd> to specify the server-side binary, and any git invocation accepts -c core.sshCommand=<cmd> to override how it connects. Mercurial accepts --config=alias.<subcmd>=!<shell> on any subcommand, which redefines the subcommand you’re running as an arbitrary shell script. ssh accepts -oProxyCommand=<cmd>. These are documented features that become attack primitives when a wrapping program passes an untrusted string into the argument list.

The failure mode has its own CWE, CWE-88, argument injection, and it’s distinct from command injection because there’s no shell involved: the wrapping program builds an argv array and calls exec directly, exactly as every “don’t use system()” guide recommends, the array reaches git intact, and git then parses one of the arguments as an option because it starts with a dash. CVE-2019-13139 in docker build is a clean example: Go’s os/exec package, an argv array, no shell, and a git-context URL whose #ref:dir fragment reached git fetch origin <ref> as --upload-pack=<cmd>.

The pattern was demonstrated across four version control systems on the same day in August 2017, when CVE-2017-1000117 (git), CVE-2017-1000116 (Mercurial), CVE-2017-9800 (Subversion), and CVE-2017-12836 (CVS) were disclosed together. Each passed a URL’s hostname to ssh as an argument, and a hostname starting with -oProxyCommand= became an ssh option. Phabricator’s post-mortem on the disclosure noted that of the three actively maintained tools, only Subversion actually added -- before the hostname in its fix; git and Mercurial validated the hostname format instead, partly because -- isn’t supported by every ssh implementation. The same write-up called the -- mechanism itself “unsafe by default”, since code without it looks correct and works fine right up until an argument starts with a dash.

Package managers

Package managers routinely take a git URL or ref as data and pass it to a subprocess: gem 'foo', git: '...' in a Gemfile, github:user/repo#ref in a package.json, and equivalents in pyproject.toml, Cargo.toml, mix.exs, Package.swift, pubspec.yaml, conanfile.py, and go.mod. The URL and ref arrive in a manifest, a lockfile, or a transitive dependency’s metadata.

Of nineteen package managers I checked, seventeen fork the git binary as their default or only path. The two that default to a library are Cargo, which uses libgit2 with an opt-in net.git-fetch-with-cli setting to fork instead, and Poetry, which switched to dulwich in 1.2.0 with a system-git-client setting to fall back. Nix uses libgit2 for reading local repositories but forks git for fetches, because libgit2 lacks git-credential helper support.

The published CVEs against package managers in this class include CVE-2021-43809 (Bundler), CVE-2021-29472 and CVE-2022-24828 (Composer), CVE-2022-36069 (Poetry), CVE-2023-5752 (pip), CVE-2022-21223 and CVE-2022-24440 (CocoaPods), and CVE-2025-68119 (Go). The Snyk research that produced several of the 2022 entries is written up here, and Sonar maintains a catalogue of the dangerous options per binary.

Of the seventeen that fork git, exactly one uses --end-of-options: Go’s cmd/go. It added -- before repository URLs in June 2019 as a general hardening pass. In January 2026 that turned out to be insufficient and --end-of-options was added across the board as the fix for CVE-2025-68119, along with HGPLAIN=+strictflags, which has restricted Mercurial’s early-option parsing since hg 4.4.2 in 2017. The commit message ends: “We should probably follow up with a more structured change to make it harder to accidentally re-introduce these issues in the future, but for now this addresses the issue at hand.”

Minimum git versions

The other package managers that guard the argument list at all use -- or a leading-dash check on the input, and looking at when each guard was added, most arrived as the fix for a reported vulnerability rather than in the original implementation. The -- before the URL in Bundler’s git clone is the CVE-2021-43809 patch. The leading-dash rejection in cocoapods-downloader landed in three commits over ten days in March 2022, matching the CVE-2022-21223 disclosure. Poetry’s guard arrived in September 2021 with a CVE assigned a year later, and the switch to dulwich followed six months after that. vcpkg is the exception I found where -- was present from the day git registry support was written.

Composer’s advisory for CVE-2022-24828 explains why almost none of these tools use --end-of-options: it names the flag as the correct fix and then says Composer supports git versions that predate it, so the patch rejects leading-dash branch names instead. vcpkg’s git integration has a comment stating a floor of git 2.7.4. Homebrew’s HOMEBREW_MINIMUM_GIT_VERSION is 2.7.0 on Linux, set in 2018.

Amazon Linux 2, which packaged git 2.14.3, reached end of life last month, so the distributions those floors track are only now ageing out. Ubuntu 18.04 with git 2.17.0 is in extended support until 2028. Ubuntu 20.04, in extended support until 2030, packages 2.25.1, which is new enough to accept --end-of-options on git fetch but old enough to reject it on git rev-parse. Relying on the flag means raising the minimum git to 2.24.0 for most subcommands, 2.30.0 for rev-parse, or 2.43.1 for checkout and reset, and losing anyone still on the distribution-packaged git.

Git libraries

libgit2, gitoxide, go-git, JGit, and dulwich all implement enough of the git wire protocol to clone and fetch in-process, with no argv boundary and so no argument list to inject into. Jujutsu uses gitoxide for its git interop and has no published CVEs in the argument-injection class; its two advisories to date are a path traversal and a missing SHA-1 collision check inherited from the library. go-git has one, CVE-2025-21613, and it’s specifically on the file:// transport, the one code path in go-git that spawns the git binary.

I noted in the package manager CWEs post that this trades one problem for another, since a bundled git implementation has to track every checkout-safety fix upstream git ships, and libgit2 and JGit have both had rounds of those. That’s a real cost, but it’s a stream of specific patches to apply rather than a check that has to be remembered at every call site forever.

Writing this prompted me to open a PR against Homebrew raising its minimum git to 2.30.0 and adding --end-of-options before URLs in clone, remote set-url, and ls-remote, and before refs in rev-parse. The checkout and reset calls are left alone, since covering those would need a floor of 2.43.1, released February 2024, and that’s recent enough to still be ahead of several supported distributions.

联系我们 contact @ memedata.com