Git에서 --end-of-options가 필요한 이유
5 hours ago
2
- Git의 --는 일반적인 옵션 종료자가 아니라 리비전과 경로 명세를 구분하므로, 신뢰할 수 없는 리비전을 안전하게 전달하려면 Git 2.24.0부터 지원된 --end-of-options가 필요함
- git log --end-of-options "$rev" -- "$path"에서 앞 표식은 옵션과 리비전, 뒤의 --는 리비전과 경로를 구분하며 서로 대체할 수 없음
- 셸 없이 argv 배열을 직접 실행해도 대시로 시작하는 입력이 --upload-pack, core.sshCommand, ProxyCommand 같은 옵션으로 해석되면 CWE-88 인자 주입이 발생할 수 있음
- 조사한 패키지 관리자 19개 중 17개가 기본 또는 유일한 방식으로 Git 바이너리를 실행하지만, --end-of-options를 사용하는 도구는 Go의 cmd/go 하나뿐이었음
- 근본 대응에는 서브명령에 따라 Git 최소 버전을 2.24.0·2.30.0·2.43.1로 높여야 하는 호환성 비용이 있으며, Git 라이브러리는 인자 주입 경계를 없애는 대신 upstream의 체크아웃 안전성 수정을 직접 추적해야 함
--와 --end-of-options의 차이
- 일반적인 Unix 도구에서 --는 옵션 해석의 끝을 표시하므로 rm -- -f는 -f를 강제 삭제 옵션이 아닌 파일명으로 처리함
- Git은 일찍부터 --를 리비전과 경로 명세(pathspec)의 구분자로 사용함
- git log foo는 foo라는 브랜치와 파일 중 무엇을 뜻하는지 모호함
- git log main -- README.md는 main의 커밋 중 README.md를 건드린 커밋을 뜻함
- 이 설계 때문에 리비전 위치에는 옵션 종료 표식이 없었고, git log "$rev"에서 $rev가 대시로 시작하면 Git이 옵션으로 해석함
- --end-of-options를 도입한 커밋은 기존 --가 이미 리비전과 경로 명세를 나누고 있어 옵션과 리비전을 구분할 별도 표식이 필요하다고 밝힘
- --end-of-options는 gitcli(7)에 문서화됐으며, Git 2.24.0이 출시된 2019년 11월 추가됨
명령별 올바른 사용법
- git clone -- "$url"은 clone이 POSIX 관례를 따르므로 URL 앞의 --가 옵션 해석을 종료함
- git checkout "$ref" --의 뒤쪽 --는 $ref를 파일명이 아닌 리비전으로 표시하지만, $ref가 먼저 옵션으로 해석되는 것은 막지 못함
- 신뢰할 수 없는 리비전과 경로를 함께 안전하게 전달하려면 git log --end-of-options "$rev" -- "$path"처럼 두 표식을 모두 사용해야 함
- --end-of-options는 옵션과 리비전을 구분함
- --는 리비전과 경로를 나눔
- 두 표식을 서로 바꿔 쓸 수 있다고 취급하면 대시로 시작하는 입력을 차단하지 못함
서브명령마다 다른 지원 시점
- --end-of-options 지원은 모든 Git 명령에 한꺼번에 적용되지 않고 서브명령별로 추가됨
- git rev-parse는 자체 인자 파서를 사용해 최초 도입보다 1년 늦은 Git 2.30.0에서 지원하기 시작함
- git checkout과 git reset은 자체적으로 --를 해석하며, 초기 구현이 --end-of-options를 인자 목록에 남겨 두는 바람에 이를 거부함
셸 없이도 발생하는 인자 주입
- Git, Mercurial, SSH에는 호출자가 지정한 명령을 실행하는 옵션이 공식 기능으로 존재함
- git clone --upload-pack=<cmd>는 서버 측 바이너리를 지정함
- 모든 Git 호출의 -c core.sshCommand=<cmd>는 연결 명령을 바꿈
- Mercurial의 --config=alias.<subcmd>=!<shell>은 실행할 서브명령을 임의의 셸 스크립트로 재정의함
- SSH의 -oProxyCommand=<cmd>는 프록시 명령을 지정함
- 래퍼 프로그램이 신뢰할 수 없는 문자열을 인자 목록에 넣으면 이러한 기능이 공격 수단으로 바뀔 수 있음
- 이 실패 유형은 CWE-88 인자 주입(argument injection) 에 해당하며 셸 명령 주입과 다름
- 프로그램이 system() 대신 argv 배열과 exec를 사용해도 발생함
- 배열은 손상 없이 Git에 전달되지만, Git이 대시로 시작하는 인자를 옵션으로 해석함
- docker build의 CVE-2019-13139는 Go의 os/exec와 argv 배열을 사용하고 셸을 거치지 않은 사례임
- Git 컨텍스트 URL의 #ref:dir 조각이 git fetch origin <ref>에 전달되면서 <ref>가 --upload-pack=<cmd>로 해석됨
여러 버전 관리 시스템에서 반복된 취약점
- 2017년 8월 같은 날 네 개 버전 관리 시스템에서 동일한 패턴이 공개됨
- 네 시스템 모두 URL의 호스트명을 SSH 인자로 전달했고, -oProxyCommand=로 시작하는 호스트명이 SSH 옵션으로 처리됨
- Phabricator 사후 분석에 따르면 당시 활발히 유지되던 세 도구 중 Subversion만 호스트명 앞에 --를 추가함
- Git과 Mercurial은 모든 SSH 구현이 --를 지원하지 않는다는 이유도 있어 호스트명 형식을 검증함
- --가 빠진 코드도 인자가 대시로 시작하기 전까지는 정상적으로 보이고 작동하므로, 이 메커니즘은 기본적으로 안전하지 않음
패키지 관리자가 노출되는 경로
- 패키지 관리자는 매니페스트, 잠금 파일, 전이 의존성 메타데이터에서 Git URL이나 ref를 받아 하위 프로세스에 전달함
- Gemfile의 gem 'foo', git: '...'
- package.json의 github:user/repo#ref
- pyproject.toml, Cargo.toml, mix.exs, Package.swift, pubspec.yaml, conanfile.py, go.mod의 동등한 설정
- 2026년 7월 HEAD 기준 조사한 19개 패키지 관리자 중 17개가 Git 바이너리를 기본 또는 유일한 경로로 실행함
- 나머지 두 개는 라이브러리를 기본으로 사용함
- Cargo는 libgit2를 사용하며 net.git-fetch-with-cli를 활성화하면 Git 프로세스를 실행함
- Poetry는 1.2.0부터 dulwich로 전환했고 system-git-client 설정으로 시스템 Git을 사용할 수 있음
- Nix는 로컬 저장소를 읽을 때 libgit2를 쓰지만, libgit2가 git-credential 도우미를 지원하지 않아 fetch에는 Git 프로세스를 실행함
- 조사 대상은 Bundler, Cargo, CocoaPods, Composer, Conan, Go, Helm, Homebrew, Mix, Nix, npm, pip, pnpm, Poetry, Pub, SwiftPM, uv, vcpkg, Yarn임
패키지 관리자에서 확인된 CVE
실제 방어 현황과 Go의 수정
- Git 프로세스를 실행하는 17개 패키지 관리자 중 --end-of-options를 사용한 도구는 Go의 cmd/go 하나였음
- Go는 2019년 6월 일반적인 방어 강화 차원에서 저장소 URL 앞에 --를 추가함
- 2026년 1월 --만으로는 충분하지 않다는 사실이 드러나 CVE-2025-68119 수정으로 --end-of-options를 전반적으로 추가함
- 같은 수정에는 HGPLAIN=+strictflags도 포함됨
- Go 수정 커밋은 같은 문제가 재도입되기 어렵도록 더 구조적인 변경이 필요할 수 있지만, 우선 현재 문제를 해결함
대부분의 방어는 취약점 공개 뒤 추가
- 나머지 패키지 관리자들은 인자 목록을 보호하더라도 주로 -- 또는 입력의 선행 대시 거부를 사용함
- Bundler의 git clone URL 앞 --는 CVE-2021-43809 패치로 추가됨
- cocoapods-downloader의 선행 대시 거부는 CVE-2022-21223 공개 시기와 맞물린 2022년 3월 열흘 동안 세 개 커밋으로 적용됨
- Poetry의 방어는 2021년 9월 추가됐고 1년 뒤 CVE가 배정됐으며, 6개월 뒤 dulwich로 전환함
- vcpkg는 예외적으로 Git 레지스트리 지원이 작성된 첫날부터 --를 사용함
최소 Git 버전이 만드는 호환성 제약
- Composer의 CVE-2022-24828 권고문은 --end-of-options를 올바른 수정으로 지목하지만, 더 오래된 Git도 지원해야 하므로 대시로 시작하는 브랜치명을 거부하는 방식을 택함
- vcpkg의 Git 통합은 최소 버전을 Git 2.7.4로 명시하며, Homebrew의 Linux용 HOMEBREW_MINIMUM_GIT_VERSION은 2018년에 설정된 2.7.0임
- Git 2.14.3을 제공한 Amazon Linux 2가 2026년 6월 수명을 종료하면서 이러한 하한선이 추적하던 배포판들이 이제야 지원 범위에서 빠지고 있음
- Ubuntu의 장기 지원 상태도 일괄적인 전환을 어렵게 함
- Ubuntu 18.04는 Git 2.17.0을 제공하며 2028년까지 연장 지원됨
- Ubuntu 20.04는 Git 2.25.1을 제공하며 2030년까지 연장 지원됨
- Git 2.25.1은 git fetch의 --end-of-options를 받아들이지만 git rev-parse에서는 거부함
- --end-of-options에 의존하려면 대부분의 서브명령은 Git 2.24.0, rev-parse는 2.30.0, checkout과 reset은 2.43.1을 최소 버전으로 요구해야 함
- 최소 버전을 높이면 배포판에 포함된 오래된 Git을 사용하는 이용자를 지원할 수 없게 됨
프로세스 실행 대신 Git 라이브러리 사용
- libgit2, gitoxide, go-git, JGit, dulwich는 프로세스 내부에서 clone과 fetch에 필요한 Git 전송 프로토콜을 구현함
- 별도의 argv 경계가 없어 인자 목록에 주입할 대상 자체가 없음
- Jujutsu는 Git 연동에 gitoxide를 사용하며 인자 주입 유형의 공개 CVE가 없음
- 현재까지의 권고 두 건은 경로 순회와 라이브러리에서 물려받은 SHA-1 충돌 검사 누락임
- go-git의 CVE-2025-21613은 file:// 전송에 한정됨
- 이 경로는 go-git에서 Git 바이너리를 실행하는 유일한 코드 경로임
- 자체 Git 구현을 포함하면 upstream Git이 내놓는 모든 체크아웃 안전성 수정을 추적해야 하며, libgit2와 JGit 모두 이와 관련된 수정이 반복된 적이 있음
- 이 비용은 실제로 존재하지만, 각 호출 지점에서 영구적으로 인자 검사를 기억하는 대신 구체적인 upstream 패치 흐름을 적용하는 문제로 바뀜
Homebrew에 제안한 변경 범위
- Homebrew PR은 최소 Git 버전을 2.30.0으로 높이고 다음 위치에 --end-of-options를 추가함
- clone, remote set-url, ls-remote의 URL 앞
- rev-parse의 ref 앞
- checkout과 reset 호출은 변경하지 않음
- 두 명령까지 보호하려면 2024년 2월 출시된 Git 2.43.1이 필요함
- 이 버전은 현재 지원되는 여러 배포판이 제공하는 Git보다 최신임
-
Homepage
-
개발자
- Git에서 --end-of-options가 필요한 이유