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 2.43.1이 출시된 2024년 2월에 해결됨

셸 없이도 발생하는 인자 주입

  • 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도 포함됨
    • 이 설정은 Mercurial 4.4.2가 출시된 2017년부터 Mercurial의 초기 옵션 해석을 제한함
  • 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보다 최신임
Read Entire Article