인지야공/오늘의 AI 이슈/5번째 글
Plugin4Shell — SHA로 못박은 플러그인이 어떻게 뚫렸나
실험:
python 딥러닝/daily/2026-09-20_plugin4shell.py(git 필요, 수 초) 글의 수치·재현은 전부 그 스크립트를 돌려 얻은 것이고, 사건 내용은 2026년 9월 20일까지의 1차 자료로 교차 확인했다.
1. 무슨 일이 있었나
2026년 9월 18일, 보안 기업 AIR 이 Plugin4Shell 을 공개했다. AI 코딩 에이전트 네 개 — Claude Code · OpenAI Codex · GitHub Copilot · Gemini CLI — 에 걸린 제로클릭 원격 코드 실행(RCE) 이다. 사용자가 클릭·승인·재설치를 하나도 하지 않아도 악성 플러그인 코드가 실행된다. AIR 은 이를 “첫 유형의 AI 공급망 공격”이라 불렀다.
| 에이전트 | 상태 |
|---|---|
| Claude Code | 패치됨 (2.1.179) |
| OpenAI Codex | 패치됨 (0.146.0) |
| GitHub Copilot | 미패치 (저장소 이름 제약이 공격면을 줄인다는 입장) |
| Gemini CLI | 미패치 (지원 종료 → Antigravity 로 이전 안내) |
놀라운 점은 암호 해시를 깨지 않았다는 것이다. 플러그인을 안전하게 묶어 둔다는 SHA 고정(SHA pinning) 을, 해시는 그대로 둔 채 git 의 허점으로 우회했다. 이 글은 그 구조를 실제 git 으로 재현한다.
2. SHA 고정은 무엇이고 왜 믿었나
플러그인 마켓은 코드를 특정 커밋에 못박아 신뢰를 만든다.
한 커밋의 코드를 검토하고, 그 커밋을 핀으로 고정하고, 그 핀이 영원히 그 코드라고 믿는다.
에이전트는 이렇게 설치한다.
git clone <플러그인 저장소>
git checkout 578b3c30ae24f54b5075d663f79d37d2a7fa0b94 # 40자리 커밋 해시(핀)
이 40자리 16진수는 커밋의 SHA-1 해시다. git 은 모든 것을 내용으로 주소를 매기는(content-addressed) 방식으로 저장한다 — 내용이 1바이트라도 바뀌면 해시가 완전히 달라진다. 그래서 “이 해시 = 이 코드”라는 약속(commitment) 이 성립한다. 이 약속을 깨려면 같은 해시를 갖는 다른 코드를 만들어야 하는데, 그건 해시 충돌을 찾는 일이다.
얼마나 어려운가. 비트 해시에서 충돌을 찾는 데 필요한 시도 수는 birthday bound(생일 문제에서 나오는 경계)로 정해진다.
| 기호 | 뜻 |
|---|---|
| 해시 비트 수 (SHA-1 은 160비트) | |
| birthday paradox: 전수()가 아니라 제곱근만큼만 모아도 충돌이 생긴다 |
직접 재 보기 B (먼저, 왜 해시는 못 깨나)
SHA-1 을 비트로 잘라, 무작위 입력으로 첫 충돌까지 걸린 시도 수를 실측해 birthday bound 와 비교했다.

| 비트 | 8 | 16 | 24 | 32 | 로그 기울기 |
|---|---|---|---|---|---|
| 실측 시도 수 | 20.1 | 324.1 | 5,296.5 | 80,770.9 | 0.504 |
| birthday bound | 20.1 | 320.8 | 5,133.6 | 82,137.2 | (이론 0.5) |
실측 기울기가 0.504 ≈ 0.5 로, 비트가 늘 때마다 시도 수가 로 지수적으로 커진다. 40자리 = 160비트라 위조에는 약 번이 필요하다. 불가능하다. 그러니 공격자는 해시를 깨지 않았다. 대신 해시를 아예 건드리지 않는 길을 찾았다.
3. 진짜 허점 — 이름과 주소가 겹친다
git 에서 578b3c... 같은 문자열은 두 가지로 읽힐 수 있다.
- 객체 이름(object id): 그 해시를 가진 커밋
- 참조 이름(ref, 브랜치 이름): 우연히(혹은 고의로) 그렇게 이름 붙인 브랜치
git 의 규칙은 이렇다.
한 이름이 유효한 참조이면서 동시에 객체 id 이면, git 은 참조를 우선하고
refname is ambiguous경고 한 줄만 낸다.
공격자는 자기 저장소에 핀 해시와 똑같은 이름의 브랜치를 만들어 악성 커밋을 가리키게 하고, 그걸 기본
브랜치로 둔다. 그러면 git checkout <핀 해시> 는 핀 커밋이 아니라 그 브랜치를 꺼내 온다. 게다가 체크아웃은
성공한 것처럼 보인다 — 핀 커밋이 저장소에 남아 있든 말든, 브랜치가 꺼내진다.
직접 재 보기 A (실제 git 으로 재현)
git 2.52 로, 핀 커밋(SAFE)과 똑같은 이름의 브랜치가 악성 커밋(MALICIOUS)을 가리키게 만들고 체크아웃했다.
핀 커밋 578b3c30ae24f54b5075d663f79d37d2a7fa0b94 (SAFE original code)
악성 커밋 63a2d925860abe1b0da8052a4f2cd3dc4b45fe1a (MALICIOUS payload)
$ git checkout 578b3c30ae24f54b5075d663f79d37d2a7fa0b94
warning: refname '578b3c30…' is ambiguous. ← 경고는 이 한 줄뿐
Switched to branch '578b3c30…'
$ cat plugin.js
MALICIOUS payload ← 핀은 SAFE인데 받은 건 악성
$ git rev-parse HEAD
63a2d925… ← HEAD는 악성 커밋
핀은 SAFE 를 가리키는데, 실제로 실행되는 코드는 MALICIOUS 다. 해시는 멀쩡하다. 이름이 주소를 가린 것이다. 이것은 권한 단조성 편에서 다룬 권한의 도달 범위 문제와 같은 결이다 — 신뢰의 근거 (핀 해시)가 있어도, 그 근거를 해석하는 단계에 구멍이 있으면 신뢰가 통째로 새어 나간다.
4. 어떻게 막나
AIR 이 짚은 핵심은 검증이 에이전트(클라이언트) 안에서 일어나야 한다는 것이다. 핀은 클라이언트에서 풀리므로 마켓이 대신 보장해 줄 수 없다.
직접 재 보기 C
같은 공격 상태에서 세 방식을 비교했다.
| 방식 | 결과 |
|---|---|
순진한 git checkout <해시> | MALICIOUS payload (뚫림) |
커밋으로 못박기 <해시>^{commit} | SAFE original code |
체크아웃 뒤 HEAD == 핀 검증 | 불일치 → 중단 |
<해시>^{commit}처럼 객체로 못박아 모호성을 없애거나,- 체크아웃 뒤
git rev-parse HEAD가 핀과 정확히 같은지 확인하고 다르면 중단한다.
두 번째가 AIR 이 권한 방어다 — 한 줄이면 된다. Claude Code(2.1.179)와 Codex(0.146.0)는 이 계열의 검증을 넣어 막았다.
5. 흔한 오해와 한계
- “SHA-1 이 깨졌다” — 아니다. 2절에서 봤듯 위조엔 이 필요하다. 이 공격은 해시를 건드리지 않는다. 허점은 암호가 아니라 git 의 이름 해석(ref vs object) 에 있다.
- “충돌 저항이면 충분하다” — 아니다. 해시가 튼튼해도 그 해시를 어떻게 조회하느냐에 구멍이 있으면 소용없다. 보안은 가장 약한 해석 단계에서 뚫린다.
- “AI 에이전트만의 문제” — 근본은 git 의 오래된 모호성이다. 다만 에이전트는 사람의 확인 없이 자동으로 checkout·설치를 하므로, 경고 한 줄이 그냥 삼켜져 제로클릭이 된다. 자동화가 위험을 키웠다.
- 미패치 남음 — Copilot·Gemini CLI 는 이 글 시점에 패치가 없다. 마켓이 다른 곳(예: Bitbucket)에 있으면 “이름 제약” 논리도 통하지 않는다.
- 이 글의 실험 — 실제 공격이 아니라, 로컬 임시 저장소에서 이름 모호성 메커니즘을 재현하고 birthday bound 를 계량한 것이다.
6. 한 문단 요약
Plugin4Shell 은 AI 코딩 에이전트가 플러그인을 커밋 해시에 못박아 두던 신뢰를 무너뜨렸다. 그런데 해시를 깬 게
아니다 — 재 보니 160비트 해시 위조엔 번이 필요해(birthday bound 기울기 실측 0.504) 사실상 불가능하다. 공격은
암호를 우회했다. git 에서 578b3c… 같은 문자열은 커밋(주소) 도 되고 브랜치(이름) 도 되는데, 둘이 겹치면
git 은 이름을 우선하고 경고 한 줄만 낸다. 공격자가 핀 해시와 똑같은 이름의 브랜치로 악성 커밋을 가리키면,
git checkout <핀> 은 핀은 SAFE 라고 보고하면서 악성 코드를 꺼내 온다(실제 재현 확인). 사람이라면 경고를
봤겠지만, 에이전트는 자동으로 삼켜 제로클릭이 된다. 방어는 단순하다 — 체크아웃 뒤 HEAD 가 핀과 같은지
클라이언트에서 확인하고 다르면 중단한다. 신뢰의 근거가 튼튼해도, 그 근거를 해석하는 마지막 단계가 무르면
전부 새어 나간다.
참고
- Plugin4Shell — AIR Security (2026-09-18) — 원 보고서
- Zero-click RCE vulnerability hit four major AI coding agents — Help Net Security
- Plugin4Shell Zero-Click RCE Hits Claude Code, Codex, Copilot and Gemini CLI — Cyber Security News
- A zero-click RCE flaw in AI coding agents — InfoWorld
- git rev-parse — 리비전 지정과 모호성 규칙 (공식 문서)
- 관련 글: 권한 단조성 · 명세 게이밍