| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2026-28292 |
| 영향 제품/버전 | npm simple-git 3.15.0 이상 3.32.3 미만 |
| CVSS v3.1 | 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H |
| CWE | CWE-78 (OS 명령 주입), CWE-178 (대소문자 민감성의 부적절한 처리) |
| 공개일 | 2026-03-10 (GitHub Security Advisory/NVD) |
| 수정 버전 | 3.32.3 |
| 수정 커밋 | f7042088aa2dac59e3c49a84d7a2f4b26048a257 (2026-02-23) |
| 취약 지점 | simple-git/src/lib/plugins/block-unsafe-operations-plugin.ts의 preventProtocolOverride() |
simple-git은 Node.js 애플리케이션이 Git 실행 파일을 호출하도록 감싸는 라이브러리다. 사용자가 제어한 customArgs를 Git 명령 인수로 전달하는 애플리케이션은, 라이브러리가 protocol.allow 설정 변경을 막아 줄 것이라고 기대할 수 있다. 그러나 차단기의 정규식은 소문자 protocol만 인식했고, Git은 설정 키를 대소문자 없이 해석한다. 따라서 대문자 또는 혼합 대소문자 설정은 차단기를 통과하지만 Git에는 같은 설정으로 받아들여져, 기본적으로 금지된 외부(ext) 전송 프로토콜을 활성화하고 애플리케이션 프로세스 권한으로 임의 명령 실행에 이를 수 있었다.
simple-git의 clone(), fetch(), pull() 같은 API는 Git의 네트워크 기능을 직접 구현하는 것이 아니다. 호출자가 준 원격 URL과 옵션을 모아 대략 다음 형태의 Git 명령을 만들고, Node.js 프로세스의 권한으로 Git을 실행한다. 따라서 URL이나 추가 인수의 일부가 신뢰 경계를 넘어 들어온다면, 그 값은 단순 문자열이 아니라 Git의 동작 정책을 바꿀 수 있는 입력이다.
Node.js 애플리케이션
│ simpleGit().clone(remote, destination, customArgs)
▼
simple-git: Git 하위 명령 + 호출자 제공 인수 조립
│
▼
자식 프로세스: git clone ...
│
▼
Git: URL 해석 · 설정 적용 · 전송 프로토콜 선택
customArgs 자체가 항상 취약한 것은 아니다. 신뢰하는 프로그램이 고정된 인수를 넣는 것은 정상 사용이다. 문제는 웹 요청, CI 변수, 저장소 메타데이터 등 공격자가 바꿀 수 있는 값을 검증 없이 이 인수 배열에 넣고도, 라이브러리의 안전 플러그인만을 마지막 방어선으로 믿는 경우다.
Git의 -c key=value 옵션은 한 번의 Git 실행에만 적용되는 설정값을 명령줄에서 주입한다. protocol.allow은 명시적으로 개별 정책이 없는 전송 프로토콜의 기본 허용 정책을 정한다. Git 공식 문서는 http, https, git, ssh를 기본 허용하고, 외부 프로그램을 실행할 수 있는 ext 프로토콜은 기본 거부(never)한다고 설명한다.
즉 정상 상태에서는 원격 URL이 외부 helper를 실행하게 하는 경로가 정책상 닫혀 있다. 반면 protocol.allow=always 같은 명령줄 설정은 그 기본 정책을 바꿔 ext도 사용 가능하게 만든다. ext 전송은 원격과 통신하기 위해 외부 명령을 helper로 기동할 수 있으므로, 공격자가 URL과 해당 설정을 함께 통제하면 Git 프로세스의 권한으로 명령이 실행될 수 있다.
[기본 정책]
Git -c (없음) clone <remote>
├─ https/ssh/git → 허용
└─ ext → 기본값 never → helper 실행 전 거부
[정책을 바꾼 경우]
Git -c protocol.allow=always clone <attacker-controlled remote>
└─ ext → always → 외부 helper 기동 가능
보안 필터는 해석기보다 좁은 언어를 인식하면 안 된다. Git은 설정 키 이름을 정규화하여 대소문자를 구별하지 않는다. 그러므로 Git 입장에서 protocol.allow, PROTOCOL.ALLOW, Protocol.Allow은 같은 의미다. 하지만 패치 전 simple-git 정규식은 JavaScript의 기본 동작대로 대소문자를 구분했다.
입력 인수 값 simple-git 필터 Git 설정 해석
------------------------ ------------------------- ----------------------
protocol.allow=always "protocol"과 일치 → 차단 protocol.allow로 적용
PROTOCOL.ALLOW=always "protocol"과 불일치 → 통과 protocol.allow로 적용
Protocol.Allow=always "protocol"과 불일치 → 통과 protocol.allow로 적용
이처럼 필터의 문자열 비교 규칙과 실제 소비자의 의미 규칙이 다르면, 필터가 막았다고 생각한 위험한 의도가 다른 표기로 전달된다. 이 CVE의 본질은 Git의 결함이 아니라, Git의 정규화 규칙을 알지 못한 래퍼 측 방어의 불일치다.
blockUnsafeOperationsPlugin은 -c 다음 인수가 protocol.*.allow 설정인지 검사하고, 허용되지 않은 프로토콜 재정의라면 Git 실행 전에 오류를 던진다. 문제의 분기는 다음과 같았다.
function preventProtocolOverride(arg: string, next: string) {
if (arg !== '-c') {
return;
}
if (!/^\s*protocol(.[a-z]+)?.allow/.test(next)) {
return; // "protocol"이 소문자가 아니면 여기서 통과
}
throw new GitPluginError(/* allowUnsafeExtProtocol 필요 */);
}정규식은 protocol과 allow를 소문자로만 검사하며 /i 플래그가 없다. JavaScript 정규식 엔진에는 일치하지 않는 대문자 표기가 return을 타고, 그 뒤의 안전 플러그인은 해당 -c 값을 위험한 정책 변경으로 분류하지 않는다. 이후 simple-git은 원래 의도대로 인수 배열을 Git 자식 프로세스에 전달한다.
이 플러그인이 지켜야 할 불변식은 다음과 같다.
호출자가 명시적으로 위험한 프로토콜 재정을 허용하지 않은 경우, Git이
protocol.allow또는protocol.<name>.allow로 해석할 모든-c인수는 Git 실행 전에 거부되어야 한다.
패치 전에는 그 집합을 소문자 표기만으로 근사했다. 따라서 공격자는 애플리케이션이 전달한 추가 인수에서 설정 키의 대소문자만 바꾸고, Git이 수용하는 외부 전송 URL을 결합할 수 있었다. 구체적 실행 문자열은 재현하지 않았으며, 아래는 공개 권고와 upstream 소스에 근거한 정적 흐름이다.
[패치 전]
신뢰하지 않는 입력
└─ customArgs: ["-c", "PROTOCOL.ALLOW=always"]
+ 공격자 제어 원격 URL
│
▼
simple-git preventProtocolOverride()
/^\s*protocol(.[a-z]+)?.allow/.test("PROTOCOL.ALLOW=always")
│
├─ false: 대문자라서 필터가 설정 변경으로 인식하지 못함
▼
Git 자식 프로세스에 인수 전달
│
▼
Git 설정 파서: 키를 대소문자 없이 해석
"PROTOCOL.ALLOW" → "protocol.allow"
│
▼
ext 프로토콜 정책이 always가 됨 → 외부 helper 실행 경로가 열림
│
▼
애플리케이션(Node.js) 프로세스 권한의 명령 실행
영향은 호출 환경에 종속된다. 공격자가 customArgs와 원격 URL 모두에 영향을 줄 수 있어야 하며, 그 애플리케이션이 simple-git을 사용하는 호스트에서 Git을 실행해야 한다. 이 조건이 충족되면 Git 명령이 실패하더라도 외부 helper는 그 전에 기동될 수 있으므로, 단순히 Git 명령의 최종 성공/실패만으로 안전하다고 판단할 수 없다.
upstream 커밋 f7042088aa2dac59e3c49a84d7a2f4b26048a257은 정규식에 대소문자 무시 플래그 /i를 추가했다. 이 변경은 필터의 인식 범위를 Git 설정 키의 대소문자 비구분 의미와 맞춘다.
- if (!/^\s*protocol(.[a-z]+)?.allow/.test(next)) {
+ if (!/^\s*protocol(.[a-z]+)?.allow/i.test(next)) {
return;
}동일 커밋은 소문자, 대문자, 혼합 표기 다섯 가지를 단위 테스트에 넣어 모두 차단되는지 확인한다. 또 unsafe.allowUnsafeProtocolOverride: true를 명시한 경우에만 의도적으로 해당 설정을 통과시킨다. 즉 패치는 단순히 더 넓게 막는 것이 아니라, 위험한 동작을 허용해야 하는 호출자가 opt-in하도록 기존 설계의 경계를 보존한다.
[패치 후]
신뢰하지 않는 입력: "PROTOCOL.ALLOW=always"
│
▼
/^\s*protocol(.[a-z]+)?.allow/i
│
├─ true: 대소문자와 무관하게 정책 재정의로 인식
▼
GitPluginError → Git 자식 프로세스를 시작하지 않음
예외: 호출자가 unsafe.allowUnsafeProtocolOverride=true를 명시
└─ 위험을 인지한 신뢰 경로에서만 Git에 전달
운영자는 simple-git을 3.32.3 이상으로 업데이트해야 한다. 업데이트 전후와 관계없이, 외부 요청·PR 메타데이터·CI 환경 변수에서 받은 값을 customArgs에 직접 넣지 말고 애플리케이션별 allowlist로 옵션과 값의 형식을 제한해야 한다. 신뢰하지 않는 URL을 clone해야 하는 서비스라면 Git 실행 권한을 최소화하고, 네트워크·파일 시스템·비밀값 접근을 분리하는 것도 추가 방어층이 된다.
공개 GitHub Security Advisory에는 이 우회를 확인한 예제 코드와 결과가 포함되어 있다. 이 보고서는 이를 실행하지 않았고, advisory 설명·NVD 메타데이터·upstream 수정 diff 및 테스트만 정적으로 대조했다. 공개 예제의 핵심 조건은 (1) 공격자가 -c 설정 인수와 원격 URL에 영향을 주고, (2) 설정 키에 필터가 인식하지 못하는 대소문자 표기를 사용하며, (3) Git의 외부 전송 helper가 정책 변경으로 허용되는 것이다.
- GHSA-r275-fr43-pm7q — GitHub Security Advisory — 영향 범위, 원인, 공개 검증 설명
- CVE-2026-28292 — NVD — CVSS, CWE, 영향을 받는 버전 및 참조
- 수정 커밋
f7042088— steveukx/git-js —/i수정과 대소문자 회귀 테스트 - Git
protocol.allow공식 문서 — 기본 프로토콜 정책과ext의 기본 거부 - CodeAnt AI 기술 분석 — 공개 발견자 분석