받은 폴더를 여는 순간, 코딩 에이전트 7종이 뚫렸다

동료가 압축 파일로 프로젝트를 보내왔다. 풀어서 폴더를 열었다.
거기까지다. 프롬프트는 아직 한 글자도 입력하지 않았고, “이 작업 공간을 신뢰하시겠습니까” 창은 뜨지도 않았다. 그런데 그 저장소가 지정한 명령이 이미 내 계정 권한으로 실행됐다.
2026년 9월 1일 보안 업체 매니폴드 시큐리티(Manifold Security)가 GitSpawn(깃스폰)이라는 이름으로 이 취약점 묶음을 공개했다. 프란시스코 로살레스(Francisco Rosales)가 쓴 연구 글이고, 코딩 에이전트 7종에서 8건을 찾았다. 공개 시점에 4건이 아직 안 고쳐진 상태였다.
이 글은 이런 분을 위해 썼다. 클로드 코드·커서·코덱스 같은 CLI 코딩 에이전트를 매일 쓰는 분, 그리고 깃허브에서 클론하지 않고 압축 파일이나 공유 드라이브로 코드를 주고받는 팀이다. 오늘 당장 확인할 게 있고, 그 확인은 명령어 한 줄이면 끝난다.
용어 하나만 먼저 풀자. 여기서 말하는 **RCE(원격 코드 실행)**는 “남이 정한 명령이 내 컴퓨터에서 내 권한으로 돌아간다”는 뜻이다. 내 SSH 키, 셸 설정에 넣어둔 토큰, 환경변수에 있는 클라우드 자격증명이 전부 그 명령의 사정권에 들어간다.
프롬프트를 입력하기 전에 이미 끝나 있었다
CLI 코딩 에이전트는 시작할 때 자기가 어디에 있는지부터 파악한다.
지금 브랜치가 뭔지, 어떤 파일이 수정됐는지, 저장소가 추적하는 경로가 뭔지를 알아야 첫 답을 할 수 있다. 그 정보를 모으는 가장 자연스러운 방법이 git을 부르는 것이다.
연구진이 여러 제품에서 실제로 관찰한 호출은 이런 모양이었다.
git status --porcelain=2 --branch
git diff --name-only HEAD
특별할 게 없는 명령이다. 우리도 직접 이렇게 친다.
문제는 언제 도느냐다. 어떤 제품은 폴더를 여는 순간, 어떤 제품은 첫 메시지를 보낼 때, 어떤 제품은 첫 키를 누르는 순간에 돌았다. 그리고 여러 제품에서 이 호출이 작업 공간 신뢰 프롬프트보다 먼저 실행됐다. 콴 코드(Qwen Code)의 경우엔 사용자가 인증하기도 전이었다.
승인 버튼을 누를 기회 자체가 없었다는 뜻이다.
git이 원래 그렇게 만들어져 있다
이 공격은 git의 버그를 쓰지 않는다. 문서에 적혀 있는 정상 기능을 쓴다.
core.fsmonitor는 큰 저장소를 빠르게 만드는 설정이다. 파일을 전부 훑는 대신 “뭐가 바뀌었는지”를 알려 주는 도우미 프로그램에게 물어본다. git 공식 문서(git-config)의 표현을 그대로 옮기면 이렇다.
Otherwise, this variable contains the pathname of the “fsmonitor” hook command. (그 외의 경우, 이 변수는 “fsmonitor” 훅 명령의 경로를 담는다.)
즉 이 설정의 값은 실행할 명령이다. 그리고 git은 인덱스를 갱신할 때 그 명령을 돌린다. git status와 git diff가 바로 인덱스를 갱신하는 명령이다.
마지막 조각이 결정적이다. git은 이 설정을 저장소 자신의 .git/config 에서 읽는다. 저장소가 자기 안에 실행할 명령을 적어 두면, 그 저장소에 대고 git status를 치는 쪽이 그 명령을 실행하게 된다.
정리하면 사슬은 세 칸이다.
| 단계 | 무슨 일이 벌어지나 |
|---|---|
| 1 | 저장소의 .git/config가 core.fsmonitor에 명령을 지정해 둔다 |
| 2 | 에이전트가 컨텍스트 수집용으로 git status·git diff를 서브프로세스로 띄운다 |
| 3 | git이 인덱스를 갱신하면서 1번의 명령을 호스트에서 실행한다 |
에이전트가 어느 git 명령을 골랐는지는 상관이 없다. 인덱스를 건드리는 명령이면 다 걸린다.
clone으로는 안 옮는다, 압축 파일이어야 옮는다
여기가 이 사건에서 가장 오해하기 쉬운 대목이고, 동시에 가장 반전이 큰 대목이다.
악성 URL을 클론해도 안 걸린다. fetch도 pull도 마찬가지다. git은 원격 저장소의 로컬 설정을 절대 가져오지 않는다. 그게 설계다.
걸리려면 저장소가 .git 디렉터리를 안에 품은 파일 뭉치로 도착해야 한다. 클론이 아니라 디렉터리를 통째로 옮기는 모든 경로가 그것이다.
| 안전한 경로 | 위험한 경로 |
|---|---|
git clone <URL> |
압축 파일(.zip)로 받은 프로젝트 |
git fetch / git pull |
공유 드라이브에 올라온 폴더 |
| 동기화 폴더(드롭박스·구글 드라이브 등) | |
| USB 메모리로 건네받은 디렉터리 |
연구진은 모든 개념 증명에 .zip을 썼다.
이게 왜 현실적인 위협이냐면, 우리가 실제로 그렇게 일하기 때문이다. 동료끼리 프로젝트를 압축해서 던지고, 외주 개발자가 클라이언트에게 결과물을 폴더째 넘기고, 사내 공유 드라이브에 레거시 프로젝트가 통째로 올라가 있다. 그 폴더를 열어 보는 첫 도구가 이제 에이전트다.
왜 승인 프롬프트가 이걸 못 막나
에이전트 제품들은 대체로 권한 모델을 갖고 있다. 파일을 쓰거나 명령을 실행하기 전에 물어보고, 도구 호출을 샌드박스에 가둔다.
그런데 이 실행은 그 모델을 통과하는 게 아니라 지나쳐 간다.
컨텍스트 수집은 모델이 결정한 도구 호출이 아니다. 에이전트 자기 코드가 직접 띄운 서브프로세스다. 그래서 도구 승인 목록에도 안 걸리고, 샌드박스 안쪽도 아니고, 화면에 아무것도 안 뜬다.
구스(goose)의 CVE 설명이 이 지점을 정확한 문장으로 적어 뒀다.
The command runs before goose contacts a model and without a submitted prompt, model call, tool approval, or trust prompt. (그 명령은 goose가 모델에 연결하기 전에, 제출된 프롬프트·모델 호출·도구 승인·신뢰 프롬프트 없이 실행된다.)
모델에 닿기도 전이라는 게 핵심이다. 우리가 아는 AI 보안 사고와는 층이 다르다.
제품별로 무엇이 어떻게 뚫렸나
원문이 사례로 다룬 다섯 제품의 방아쇠는 각각 달랐다.
| 에이전트 | 실행되는 시점 |
|---|---|
| 클로드 코드 | claude로 프로젝트 폴더를 열어 시작 컨텍스트를 모을 때 |
| 구스(goose) | goose review로 작업 중 변경분 리뷰를 요청할 때 |
| 헤르메스(Hermes) | 저장소를 열고 첫 메시지를 보낼 때 |
| 콴 코드(Qwen Code) | qwen으로 폴더를 열 때 — 인증 전 |
| 그록 빌드(Grok Build) | 프롬프트를 입력하기 시작하는 첫 키 |
여덟 건 중 한 건은 core.fsmonitor가 아니었다. 클로드 코드의 리뷰 경로에서 나온 건인데, 같은 성격의 다른 git 설정 키를 쓴다. 연구진은 그 키가 안 고쳐진 상태라 이름을 공개하지 않았다.
이 절제는 글 전체에 일관돼 있다. 재현용 저장소를 그대로 배포하지 않았고, 각 사례에 싱크(어디서 실행되나)와 트리거(무엇이 부르나)만 적었다.
벤더 응답 기록이 취약점보다 눈에 띈다
원문의 타임라인 표를 옮기면 이렇다. 공개 시점(2026-09-01) 기준이다.
| 에이전트 | 제보일 | 벤더 응답 | 공개 시점 상태 |
|---|---|---|---|
클로드 코드 (core.fsmonitor) |
06-26 | 같은 날 들어온 다른 제보의 중복 처리 | 패치됨 — 2.1.196 |
| 커서 | 07-08 | 먼저 들어온 제보의 중복 처리 | 패치됨 |
| 콴 코드 | 07-07 | 알리바바 SRC 접수 | 미패치 — 0.22.3에서 재확인 |
| 구스 | 07-13 | 인정, CVE 발급 | 패치됨 — 1.44.0 |
| 그록 빌드 | 07-14 | xAI가 informative로 종결한 07-01 제보의 중복 처리 | 미패치 — 1.0.13에서 재확인 |
| 클로드 코드 (리뷰 경로) | 07-15 | 내부 티켓 중복 처리 | 미패치 — 2.1.252에서 재확인 |
| 헤르메스 | 07-20 | 5개 채널로 6번 연락, 트리아지 없음 | 미패치 — 0.21.0에서 재확인 |
| 오픈AI 코덱스 | 07-20 | 먼저 들어온 제보의 중복 처리 | 패치됨 |
여덟 건 중 다섯 건이 “중복”으로 종결됐다. 그중 하나는 같은 날 다른 연구자가 낸 제보와 겹쳤다.
연구진은 이걸 이렇게 읽었다 — 한 방향에서만 발견되고 있는 게 아니다. 여러 연구자가 독립적으로 같은 자리에 도달하고 있다는 뜻이고, 그건 공격자 쪽도 마찬가지라는 뜻이다.
그리고 “중복”으로 닫힌 티켓이 고쳐졌다는 뜻은 아니었다. 그록 빌드는 07-01 제보가 informative(정보성)로 닫혔고, 07-14의 재제보는 그 닫힌 건의 중복으로 닫혔다. 두 번 닫히는 동안 코드는 그대로였다.
공개 하루 만에 올라온 커밋 하나
여기서 원문에 없는 후속이 있어서, 우리가 NVD와 깃허브로 직접 확인했다.
헤르메스는 원문 공개 시점에 미패치였다. 07-20에 제보했고, 다섯 개 채널로 여섯 번 연락했는데 비공개 보안 권고가 트리아지조차 안 됐다. CVE는 벤더가 아니라 VulnCheck라는 독립 CVE 발급기관이 매겼다.
그런데 수정 커밋 f6234d0의 작성 시각을 조회하니 2026-09-02 17:14 UTC였다. 원문 공개 다음 날이다. 커밋 메시지도 노골적이다.
fix(security): close GitSpawn RCE class —
malicious repo .git/config no longer executes on context gathering
여섯 번의 비공개 연락이 못 움직인 것을, 공개가 하루 만에 움직였다.
공개 취약점 보고(full disclosure)의 효과를 이만큼 깔끔하게 보여 주는 기록도 드물다. 동시에 그 기간 동안 사용자는 아무것도 모른 채 노출돼 있었다는 뜻이기도 하다.
NVD가 매긴 점수와, 설명에 적힌 파일 이름
CVE 두 건을 NVD에서 직접 조회했다. 원문의 주장을 3자 기록으로 대조하기 위해서다.
| CVE | 대상 | 등재일 | CVSS v4.0 | 영향 범위 |
|---|---|---|---|---|
| CVE-2026-72718 | 구스 | 2026-08-10 | 7.0 (HIGH) | 1.44.0 이전 |
| CVE-2026-71963 | 헤르메스 | 2026-09-03 | 8.6 (HIGH) | 0.18.2 ~ 0.21.0 |
헤르메스 쪽은 CVSS v3.1로도 8.8(HIGH)이 매겨져 있다. 점수가 더 높은 이유는 방아쇠가 더 흔하기 때문이다 — 리뷰 명령을 따로 칠 필요 없이, 저장소를 열고 아무 메시지나 보내면 걸린다.
NVD 설명에서 특히 눈에 띈 건 유출 대상을 명시한 대목이다.
…exposing the full environment including configured provider API keys. (설정된 프로바이더 API 키를 포함한 전체 환경이 노출된다.)
에이전트를 쓰려면 그 환경에 모델 API 키가 들어 있다. 취약점이 겨냥한 자리에 정확히 그 키가 있는 셈이다.
구스 쪽 설명은 한 발 더 나가서 취약한 함수 이름까지 적어 뒀다. crates/goose-cli/src/commands/review/handler.rs의 git_command()가 명령을 조립하고, touched_files()와 collect_diff()가 그걸 쓴다. 구스는 git에 설정 플래그를 하나 넘기긴 했다 — core.quotePath=off. 위험한 키는 하나도 안 지운 채였다.
깃스폰이 한 벤더의 실수가 아닌 이유
이 대목은 우리가 직접 확인한 것이라 근거를 밝혀 둔다.
콴 코드는 연구진이 0.22.3에서 실행에 성공했다고 적었다(08-28 릴리스). 그래서 npm에서 0.22.3과 그 다음 릴리스인 0.23.0(09-03) 패키지를 각각 내려받아 번들 안을 대조했다.
결과는 예상과 반대였다. 두 버전 모두 다음이 이미 들어 있다.
function withSafeGitConfig(args) {
return [
"-c", "core.fsmonitor=false",
"-c", "core.untrackedCache=false",
"-c", "core.quotePath=false",
...args
];
}
위험한 설정 키 목록도 있다. ^core\.(askpass|editor|fsmonitor|pager|sshcommand)$, ^core\.hookspath$, ^credential\.helper$ 같은 패턴이 배열로 정리돼 있고, 저장소의 로컬 config에 그런 값이 있는지 미리 재 보는 getLocalGitConfigRisk() 함수까지 있다.
즉 방어를 몰라서 안 넣은 게 아니다. 넣어 놨는데, 연구진이 실행에 성공한 그 경로에는 안 붙어 있었다는 뜻이 된다.
이게 깃스폰이라는 사건의 성격을 가장 잘 설명한다. 한 벤더가 게을렀던 사고가 아니라, 에이전트가 git을 부르는 자리가 코드 곳곳에 흩어져 있고 그중 하나만 빠져도 뚫리는 구조적 사고다. 원문이 “one vendor’s bug가 아니다”라고 못 박은 이유다.
(우리가 확인한 것은 두 번들에 그 헬퍼가 존재한다는 사실까지다. 0.23.0이 시작 경로의 구멍을 실제로 막았는지는 확인하지 않았다. 벤더 공지가 나오기 전까지는 막혔다고 가정하지 않는 편이 안전하다.)
숫자로 본 노출 범위
원문이 규모를 적어 뒀는데, 우리가 오늘 기준으로 다시 재 봤다.
| 항목 | 원문(09-01) | 우리 실측(09-07) |
|---|---|---|
| 클로드 코드 npm 월 다운로드 | 7,700만+ | 8,022만 640 (07-31~08-29 구간) |
| 헤르메스 깃허브 별 | 23만 7,000+ | 24만 2,674 |
| 클로드 코드 깃허브 별 | 14만 3,000+ | 14만 4,297 |
| 구스 깃허브 별 | 5만 4,000+ | 5만 3,978 |
| 콴 코드 깃허브 별 | 2만 7,000 | 2만 7,688 |
별 개수 합계가 46만 8,637개다. 원문이 “half a million에 가깝다”고 쓴 그 숫자다.
npm 다운로드 8,022만 건은 감이 잘 안 오는 크기라 독자 단위로 바꿔 보면, 한 달 30일로 나눠 하루 267만 번 설치되는 셈이다. 물론 CI 재설치가 크게 섞인 숫자라 사용자 수와 같지 않다. 그래도 이 도구가 도는 기계의 수가 어느 자리인지는 보여 준다.
버전 시각도 확인해 뒀다. 클로드 코드는 취약이 확인된 2.1.193이 06-25, 수정된 2.1.196이 06-29 릴리스다. 나흘 만에 고쳤다. 반면 리뷰 경로 건이 재확인된 2.1.252는 08-31이고, 오늘 기준 최신은 2.1.263(09-06) 이다.
프롬프트 인젝션과는 다른 층의 사고다
우리가 지난주에 다룬 클로드 코드 오토 모드 프롬프트 인젝션과 나란히 놓으면 차이가 선명하다.
| 프롬프트 인젝션 | GitSpawn | |
|---|---|---|
| 속는 주체 | 모델 | 아무도 안 속는다 |
| 실행 경로 | 모델이 도구를 호출한다 | 에이전트 코드가 서브프로세스를 띄운다 |
| 승인 프롬프트 | 오토 모드면 자동 승인된다 | 애초에 뜨지 않는다 |
| 방어 지점 | 모델 판단·권한 정책 | git 호출 인자 |
| 모델을 바꾸면 | 결과가 달라질 수 있다 | 아무 상관 없다 |
인젝션은 모델을 더 똑똑하게 만들거나 권한 정책을 조이면 개선 여지가 있다. GitSpawn은 그 어느 쪽도 소용이 없다. 모델은 이 일에 관여조차 안 했다.
원문의 마지막 문장이 그래서 무겁다. 취약점은 모델에도, 새로 등장한 무엇에도 있지 않다. 세션을 시작할 때 자기가 어디 있는지 알아내려고 띄우는 서브프로세스 — 그 평범한 배관에 있다.
받은 폴더를 열기 전에 쓰는 3초
원문이 제시한 사용자 측 조치는 한 줄이다. 파일로 받은 저장소라면, 에이전트로 열기 전에 .git/config를 본다.
실무에서 쓸 수 있게 풀어 쓰면 이렇다.
1) 받은 폴더의 로컬 설정부터 본다. 에이전트를 띄우지 말고 이것부터 친다.
cat <받은폴더>/.git/config
프로그램 이름이나 경로를 값으로 갖는 항목이 있으면 멈춘다. fsmonitor, hooksPath, pager, editor, sshCommand, credential.helper, alias.*, diff.*.command 같은 것들이다. 정상적인 프로젝트 설정에는 대개 [core] repositoryformatversion, [remote "origin"] url 정도만 있다.
2) 훅 디렉터리도 같이 본다. 설정 키만 경로가 아니다.
ls -la <받은폴더>/.git/hooks/
.sample 확장자가 붙은 파일들만 있으면 기본 상태다. 확장자 없는 실행 파일이 있으면 내용을 확인한다.
3) 의심스러우면 .git을 지우고 다시 만든다. 이력이 필요 없는 코드라면 가장 확실하다.
rm -rf <받은폴더>/.git
이력이 필요하면 클론으로 다시 받는다. 앞에서 봤듯 클론 경로는 이 설정을 옮기지 않는다.
4) 도구를 최신으로 올린다. 클로드 코드와 코덱스·커서·구스는 해당 건이 고쳐졌다.
npm i -g @anthropic-ai/claude-code@latest
5) 자동 실행 환경을 특히 조심한다. 셀프호스티드 러너에서 에이전트를 돌리는 구성이라면, 러너가 집어 오는 코드가 아티팩트나 캐시로 도착하는 경로가 없는지 본다. 사람이 .git/config를 눈으로 볼 기회가 아예 없는 자리다.
에이전트를 만드는 쪽이 고칠 한 줄
제품을 만드는 입장이면 조치가 더 간단하다. 배경에서 도는 컨텍스트 수집 호출의 git 설정을 무력화하면 된다.
git -c core.fsmonitor=false status
다만 콴 코드 번들에서 본 것처럼, 헬퍼를 만드는 것과 모든 호출이 그 헬퍼를 지나가게 하는 것은 다른 일이다. 실제로 지켜야 할 규칙은 이쪽이다.
- git을 부르는 자리를 한 함수로 모은다. 곳곳에서 직접
spawn("git", …)을 부르면 언젠가 하나가 빠진다. - 그 함수가
-c플래그로 위험 키를 전부 지운다.core.fsmonitor하나만 지우는 건 부족하다 — 여덟 건 중 한 건이 다른 키였다. - 시작 시점에 도는 호출을 목록으로 만들어 둔다. 신뢰 프롬프트보다 먼저 도는 게 있는지가 그 목록에서 바로 보여야 한다.
팀에서 쓰는 에이전트 설정을 훅으로 표준화해 두는 구성이라면, 받은 저장소를 열기 전 검사 단계를 그 자리에 끼워 넣는 게 가장 자연스럽다.
이 패턴은 저장소에서 끝나지 않는다
원문이 마지막에 던진 지적이 이 글에서 가장 오래 남을 부분이라고 본다.
에이전트가 자기가 안 쓴 것을 집어 오는 자리는 저장소만이 아니다. 스킬·MCP 서버·플러그인도 똑같다. 파일로 도착하고, 자기 설정을 들고 오고, 도착했다는 이유만으로 신뢰받는다.
물건이 바뀔 뿐 모양은 같다. 저장소는 .git/config를 들고 오고, MCP 서버는 실행 명령이 적힌 설정을 들고 오고, 스킬은 자기 스크립트를 들고 온다. 셋 다 “이걸 열어 볼 때 무엇이 실행되는가”라는 같은 질문 앞에 있다.
MCP 명세가 상태 없는 방향으로 옮겨 간 과정을 정리할 때도 결국 같은 질문이 남았다. 연결하는 쪽이 무엇을 실행하게 되는가. 문제는 늘 모델이 아니라, 모델 주위에 붙은 실행 경로에 있었다.
아직 확인되지 않은 것
정직하게 남겨 둘 것들이다.
첫째, 실제 악용 사례는 보고된 게 없다. 연구진이 찾아 제보한 건이고, 야생에서 쓰였다는 증거는 이 자료에 없다.
둘째, 미패치 세 건의 오늘자 상태를 우리가 확인하지 못했다. 그록 빌드·콴 코드·클로드 코드 리뷰 경로다. 각 벤더가 그 뒤로 낸 릴리스가 있고 우리가 콴 코드 번들을 열어 보긴 했지만, 시작 경로가 실제로 막혔는지는 코드를 돌려 봐야 알 수 있다. 이 글은 그 실험을 하지 않았다.
셋째, 원문이 이름을 밝히지 않은 설정 키는 여전히 비공개다. 그래서 “core.fsmonitor만 확인하면 안전하다”고 말할 수 없다. 위에 적은 확인 절차에서 프로그램 경로를 값으로 갖는 항목 전체를 보라고 한 이유다.
넷째, 연구진은 이름을 밝히지 않은 다른 에이전트에서도 같은 결함을 찾았다고 적었다. 목록에 없다고 안전하다는 뜻이 아니다.
고칠 것은 도구가 아니라 순서다
받은 폴더를 에이전트로 여는 순서를 바꾸는 것이다.
지금까지는 폴더를 열고 나서 “이 저장소 뭐 하는 건지 봐 줘”라고 물었다. 이제는 cat .git/config가 먼저다. 3초 걸린다.
압축 파일로 코드를 받는 일이 없다면 깃스폰의 사정권 밖이다. 그런 팀은 사실 많지 않다.