
안녕하세요, 미니지식공간입니다.
Plugin4Shell은 플러그인 SHA 피닝(SHA pinning, 검토가 끝난 커밋 해시에 플러그인 코드를 고정하는 방식)을 통째로 우회하는 제로클릭 RCE이며, Claude Code·Codex·GitHub Copilot·Gemini CLI 네 에이전트가 같은 설계 실수를 공유한다. 보안 회사 AIR가 2026년 9월 17일 공개했고, 원인은 git checkout 이후 실제로 그 커밋에 도달했는지 확인하지 않는다는 한 가지다.
검토한 커밋을 고정했고, 고정한 커밋을 체크아웃했고, 설치는 성공했다고 보고됐다. 그런데 작업 트리에 올라온 코드는 그 커밋이 아니다. — Plugin4Shell의 전부다.
AIR가 공개한 두 변종의 실제 명령 시퀀스는 다음과 같다. 출처: air.security/blog-posts/plugin4shell
변종 A — Claude Code / Codex / GitHub Copilot
git clone <plugin repo> ./
git checkout aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
공격자는 저장소에 고정 해시와 이름이 같은 40자리 16진수 브랜치를 만들고 그것을 기본 브랜치로 지정한다. 그러면 위의 평범한 git clone이 그 이름의 로컬 브랜치를 함께 내려받고, git checkout은 같은 이름이 레퍼런스와 객체 아이디 양쪽으로 해석될 때 레퍼런스를 우선한다. 깃은 refname is ambiguous 경고만 출력하고 브랜치를 체크아웃하므로, 원래 커밋의 존재 여부는 결과에 영향을 주지 않는다.
성립 조건은 두 가지다. 브랜치 이름이 해시 모양이어도 되어야 하고(git check-ref-format은 40-hex 이름을 허용한다), 그 브랜치가 저장소의 기본 브랜치여야 한다. 기본이 아니면 원격 추적 레퍼런스로만 받아 오므로 체크아웃이 커밋으로 폴백한다.
변종 B — Gemini CLI
git clone --depth 1 <plugin repo> ./
git fetch origin 41d0bc0a4aeb2fbf797dacea39e876d98c95024b
git checkout FETCH_HEAD
git fetch는 올바른 커밋을 가져와 .git/FETCH_HEAD에 기록한다. 그런데 git checkout FETCH_HEAD가 반드시 그 파일을 읽는 것은 아니다. 저장소의 기본 브랜치 이름이 FETCH_HEAD이면 체크아웃은 브랜치로 해석되고, 방금 받아 온 커밋은 조용히 버려진다.
설치 시점 버그였다면 영향은 제한적이었을 것이다. 문제는 같은 git checkout이 백그라운드 자동 업데이트에서 다시 실행되고, 이것이 Claude Code와 Codex의 기본값이라는 점이다. 공격 체인은 다음 순서로 완성된다.
aaa...aaa로 고정된 채 심사를 통과한다.aaa...aaa에 고정된다.bbb...bbb로 핀을 갱신한다.bbb...bbb라는 이름의 브랜치를 만들어 악성 코드를 가리키게 하고 기본 브랜치로 지정한다. 원래 커밋은 건드리지 않아도 된다.bbb...bbb를 브랜치로 해석해 실행한다. 프롬프트도 클릭도 없다.저장소 장악 경로는 새로 증명할 필요도 없다. AIR는 선행 연구에서 직접 만든 스킬이 확산돼 26,000개 이상의 에이전트를 통제한 사례(The Story of Skills)와, 이미 사용 중이던 925개 스킬을 유지보수자로부터 탈취해 134,000개 에이전트에 도달한 사례(SkillJacking)를 공개한 바 있다. Plugin4Shell은 그런 탈취를 막으려고 만든 장치를 무력화한다.
| 에이전트 | 제작사 | 상태 | 기준 버전·조치 |
|---|---|---|---|
| Claude Code | Anthropic | 패치 완료 | 2.1.179 이상 (2026-06-17 확인) |
| OpenAI Codex | OpenAI | 패치 완료 | 0.146.0 이상 (2026-08-12 확인) |
| GitHub Copilot | Microsoft | 미패치 | 수정본 없음 |
| Gemini CLI | 수정 계획 없음 | 지원 종료, Antigravity 이전 권고 (2026-08-04) |
타임라인은 2026년 5월 발견(네 에이전트 전부에 대한 PoC 포함), 6월 협조 공개 통보, 9월 17일 공개다.

AIR가 제시한 근본 수정은 체크아웃 이후 작업 트리에 실제로 올라온 커밋을 다시 확인하는 것이다. 출처: air.security/blog-posts/plugin4shell
test "$(git rev-parse HEAD)" = "<pinned-sha>" || abort
요청한 레퍼런스가 아니라 해석된 HEAD 를 검사해야 한다는 점이 핵심이다. 요청값만 검사하면 Gemini CLI 변종은 그대로 통과한다. 그리고 이 검사는 에이전트 안에서 돌아야 한다. 핀은 클라이언트에서 해석되므로, 마켓플레이스는 자신이 광고하는 보증을 스스로 강제할 수 없다.
에이전트나 플러그인 러너를 직접 만드는 쪽이라면, 외부 저장소에서 코드를 끌어와 실행하는 모든 경로에 같은 사후 검증이 붙어 있는지 확인할 가치가 있다. 네 회사가 같은 실수를 했다는 것은 이 패턴이 흔하다는 뜻이다.
각 CLI의 표준 버전 확인 플래그로 기준선을 넘었는지부터 본다.
claude --version # 2.1.179 이상인지 확인
codex --version # 0.146.0 이상인지 확인
수정본이 없는 GitHub Copilot과 Gemini CLI는 설치된 서드파티 플러그인 목록을 줄이는 것이 현실적인 완화책이다. InfoWorld에 인용된 Pareekh Consulting의 Pareekh Jain은 점검 대상으로 낯선 프로세스와 네트워크 연결, 예상치 못한 플러그인 파일, 변경된 소스 저장소, 수상한 깃 활동, 개발자·클라우드 자격증명의 비정상 사용을 꼽았고, 볼 로그로 EDR·깃·CI/CD·클라우드 IAM·인증 로그를 들었다.
GitHub 측은 더 레지스터에 커밋 SHA와 유사한 버전·태그 이름 생성에 이미 제한을 걸어 두어 GitHub와 GitHub 마켓플레이스 플러그인에서는 보고된 취약점이 악용되지 않는다고 밝혔다. 실제로 GitHub는 40-hex 브랜치 이름을 거부한다.
AIR는 그 제한이 충분하지 않다고 반박한다. 마켓플레이스는 GitHub 밖에도 있고, Bitbucket이나 자체 호스팅 깃 서버는 해시 모양 브랜치 이름을 허용하기 때문이다. AIR에 따르면 Anthropic 공식 문서도 Bitbucket과 자체 호스팅 깃을 마켓플레이스 백엔드로 안내한다. 그리고 호스트 측 이름 제한은 Gemini CLI의 FETCH_HEAD 변종에 아무 효과가 없다. 어느 쪽 주장이 실무에서 더 무겁게 작용할지는 각자의 마켓플레이스 호스팅 구성에 달려 있다.
Claude Code 몇 버전부터 안전한가?
2.1.179 이상이다. Anthropic이 AIR의 제보를 받아 수정했고 AIR가 2026년 6월 17일 확인했다. 버전을 고정해 쓰는 CI 이미지나 devcontainer가 있다면 그쪽이 먼저 확인 대상이다.
GitHub 마켓플레이스만 쓰면 괜찮은가?
GitHub는 40-hex 브랜치 이름을 거부하므로 변종 A의 성립 조건 하나가 막힌다. 다만 AIR는 Bitbucket·자체 호스팅 깃을 백엔드로 쓰는 마켓플레이스가 지원되는 구성이라는 점, Gemini CLI 변종은 별개라는 점을 들어 이것만으로 충분하지 않다고 본다.
CVE 번호가 있나?
2026년 9월 18일까지 확인된 공개 자료(AIR 블로그, Help Net Security, InfoWorld)에는 Plugin4Shell에 할당된 CVE 식별자가 명시돼 있지 않다. 추후 할당 여부는 확인이 필요하다.
Plugin4Shell의 교훈은 단순하다. 핀은 약속이고, 검증은 실행이며, 약속만 있고 검증이 없으면 아무것도 지켜지지 않는다. 같은 계열의 AI 코딩 에이전트 공급망 이슈는 앞서 정리한 GitSpawn — core.fsmonitor로 터지는 AI 코딩 에이전트 RCE 편에서도 다뤘으니 이어서 보시면 맥락이 잡힐 것이다. 읽어 주셔서 감사합니다.
출처
본 글은 공개 자료를 바탕으로 정리했으며, 세부 내용·수치는 원 출처·공식 문서와 대조 확인을 권장합니다. 재현용 익스플로잇 코드는 포함하지 않았습니다.