Git에서 --end-of-options가 필요한 이유
요약
Git에서 인자 주입(Argument Injection) 취약점을 방지하기 위해 도입된 `--end-of-options` 옵션의 역할과 필요성을 설명합니다. 기존 `--` 구분자와의 차이점을 분석하고, 안전한 명령 실행을 위한 올바른 사용법을 다룹니다.
핵심 포인트
- --end-of-options는 옵션과 리비전(revision)을 구분하여 인자 주입을 방지함
- 기존 '--'는 리비전과 경로(pathspec)를 구분하는 용도로 사용됨
- 신뢰할 수 없는 리비전 전달 시 '--end-of-options'와 '--'를 모두 사용해야 안전함
- Git 서브명령별로 해당 옵션의 지원 버전이 다르므로 주의가 필요함
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보다 최신임
댓글과 토론
AI 자동 생성 콘텐츠
본 콘텐츠는 GeekNews의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기