1P by GN⁺ | ★ favorite | 댓글 1개
  • 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 foofoo라는 브랜치와 파일 중 무엇을 뜻하는지 모호함
    • git log main -- README.mdmain의 커밋 중 README.md를 건드린 커밋을 뜻함
  • 이 설계 때문에 리비전 위치에는 옵션 종료 표식이 없었고, git log "$rev"에서 $rev가 대시로 시작하면 Git이 옵션으로 해석함
  • --end-of-options를 도입한 커밋은 기존 --가 이미 리비전과 경로 명세를 나누고 있어 옵션과 리비전을 구분할 별도 표식이 필요하다고 밝힘
  • --end-of-optionsgitcli(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 checkoutgit 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/execargv 배열을 사용하고 셸을 거치지 않은 사례임
    • 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.jsongithub: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_VERSION2018년에 설정된 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, checkoutreset은 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-21613file:// 전송에 한정됨
    • 이 경로는 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 앞
  • checkoutreset 호출은 변경하지 않음
    • 두 명령까지 보호하려면 2024년 2월 출시된 Git 2.43.1이 필요함
    • 이 버전은 현재 지원되는 여러 배포판이 제공하는 Git보다 최신임

댓글과 토론

Lobste.rs 의견들
  • 요즘 점점 jj에 빠지고 있음. 이런 Git의 난잡함과 비교하면 마음이 편해질 정도이며, 아직 익히는 중이지만 의도를 잘 반영하고 원하는 작업법도 쉽게 파악할 수 있음
    특히 한 jj 명령에서 배운 지식이 다른 명령에도 자연스럽게 적용됨. git log 문서는 출력 옵션별 플래그가 폭증한 데다 커밋 범위 문법인 “특수 표기법”과 추가 필터 플래그까지 뒤섞여 있고, 다른 Git 명령으로 전이되는 지식도 적음
    반면 jj log 문서는 한 페이지에도 여백이 남을 만큼 간결함. Git의 잡동사니식 복잡성을 리비전 집합·파일 집합·출력 템플릿 DSL 세 가지로 대체했고, 이들이 jj 전반에서 일관되게 쓰여 훨씬 단순하고 조합 가능하며 직관적임. Git이 20년간 군더더기를 쌓아왔다는 점은 감안해야 하지만, jj는 이를 피하기에 훨씬 유리해 보임
  • 명령어에서 사용자 제공 인수 앞에 보통 --가 필요하다는 건 알지만, 여기에 독자적인 변형 규칙까지 요구하면 지나치게 위험한 함정이 됨
  • 도구를 지나치게 복잡하게 만든 정직한 결과임. Git은 새로 등장한 바사호 같아 보임
  • 원래 파일 인수의 모호성을 해소하려고 --를 택한 이유가 궁금함. 파일을 명시적 인수로 받는 단순한 방식 대신 이를 선택한 데 겉으로 드러나지 않는 설계상 절충이 있었는지 의문임
    • 이미 확립된 UNIX 옵션 파싱 관례를 전혀 다른 용도로 사용한 건 예상대로 어리석고, 지극히 Git다운 선택임
  • UNIX의 모든 것은 텍스트 철학이 또 문제를 일으킴. 명령줄은 구조화된 데이터의 완벽한 예인데도, 이 구덩이에서 벗어나기 위한 공동 조율 비용이 너무 큰 듯함
    1990년대 Tcl과 Scheme의 논쟁을 돌아보면 이 경우에는 “더 나쁜 것이 더 낫다”가 맞았을지도 모름. JSON과 XML 등을 포함한 S-표현식 계열이 UNIX 생태계에 거의 자리 잡지 못한 반면, Tcl은 문자열 안에 하위 언어를 계층화하는 비교적 원칙적인 접근법을 갖추고 있었음