- 들어가며 — 이슈 본문이 명령이 되는 지점
- 무대 — CI 안의 에이전트는 무엇을 쥐고 있었나
- 1단계 — "봇이면 통과"라는 한 줄
- 2단계 — 데이터가 명령이 되는 자리
- 3단계 — 읽기 전용처럼 보이는 것들이 유출 채널이다
- 폭발 반경 — 액션 저장소 자신이 그 워크플로를 쓰고 있었다
- 이건 가설이 아니다 — Cline에서 실제로 터졌다
- 같은 제품, 다른 문 — 인젝션이 아니어도 뚫립니다
- Anthropic이 실제로 넣은 방어
- 정직한 한계 — 벤더 문서가 스스로 적어 둔 것
- 그래서 에이전트를 만드는 사람은 무엇을 해야 하나
- 마치며
- 참고 자료
들어가며 — 이슈 본문이 명령이 되는 지점
2026년 6월 1일, GMO Flatt Security의 RyotaK가 Poisoning Claude Code: One GitHub Issue to Break the Supply Chain를 공개했습니다. 요지는 제목 그대로입니다 — GitHub 이슈 하나로 Claude Code GitHub Actions를 쓰는 저장소를 장악할 수 있었고, 그 대상에는 Anthropic 자신의 저장소도 포함됐습니다.
지난 글에서 웹 에이전트의 교차 사이트 프롬프트 인젝션을 다뤘습니다. 그 글이 논문 기반의 방어 설계였다면, 이번 건은 실제로 출시된 제품에서 벌어진 일이고, 타임라인·CVSS·바운티 금액·수정 커밋이 전부 공개돼 있습니다. 그리고 중요한 차이가 하나 더 있습니다 — 같은 결함 계열이 다른 제품에서 실제로 악용됐습니다.
이 글은 공격 레시피가 아닙니다. 페이로드는 싣지 않습니다. 대신 에이전트를 만드는 엔지니어에게 필요한 것만 봅니다 — 신뢰 경계가 정확히 어디서 끊겼는지, 벤더가 넣은 방어가 무엇을 막고 무엇을 못 막는지, 그리고 "이 정도면 됐다"고 말할 수 있는 지점이 있는지.
무대 — CI 안의 에이전트는 무엇을 쥐고 있었나
Claude Code GitHub Actions는 Claude Code를 CI/CD에 붙이는 워크플로입니다. 코드 리뷰 자동화, 이슈 트리아지·라벨링, 코멘트 기반 코드 생성 같은 일을 합니다. 동작 모드는 둘입니다.
- tag mode — 이슈나 PR 코멘트에서 특정 키워드(기본값은 Claude 멘션)를 언급하면 트리거됩니다.
- agent mode — 워크플로에 prompt 입력이 설정되면 트리거됩니다. 슬래시 커맨드나 정해진 작업을 돌릴 때 씁니다.
GitHub 작업을 하려면 Claude GitHub App이 저장소에 설치돼야 하고, 이 앱이 갖는 권한은 이렇습니다 — 저장소 콘텐츠(코드) 읽기·쓰기, 이슈와 PR 읽기·쓰기, 디스커션 읽기·쓰기, 워크플로(Actions) 읽기·쓰기. 별도 토큰을 지정하지 않으면 기본으로 이 앱의 토큰이 쓰입니다. 셋업은 쉬워지고, 공격면은 넓어집니다.
여기서 이미 Simon Willison이 말한 치명적 3종 세트가 갖춰집니다 — 비공개 데이터 접근, 신뢰할 수 없는 콘텐츠 노출, 외부로 통신할 수단. 셋이 한 프로세스에 모이면 인젝션 한 방이 유출로 이어집니다. CI 러너는 이 셋을 교과서적으로 전부 갖고 있습니다.
1단계 — "봇이면 통과"라는 한 줄
이 위험을 알고 있었기 때문에, 액션은 기본적으로 쓰기 권한 없는 사용자가 워크플로를 트리거하지 못하게 막았습니다. 문서의 표현은 이렇습니다 — 액션은 저장소에 쓰기 권한이 있는 사용자만 트리거할 수 있다.
이 통제는 checkWritePermissions 함수에 구현돼 있었습니다. 함수는 액터의 권한이 write나 admin인지 확인합니다. 그런데 그 앞에 이런 분기가 하나 있었습니다.
// src/github/validation/permissions.ts (수정 전)
if (actor.endsWith("[bot]")) {
core.info(`Actor is a GitHub App: ${actor}`);
return true;
}
액터 이름이 [bot]으로 끝나면 실제 권한과 무관하게 무조건 통과입니다. 언뜻 합리적으로 보입니다 — GitHub App은 보통 저장소 관리자가 설치한 신뢰된 주체니까요. 문제는 그 가정이 공개 저장소에서 성립하지 않는다는 것입니다.
GitHub 문서가 명시하듯, GitHub App은 기본 권한이 없어도 공개 리소스를 읽을 암묵적 권한을 갖습니다. 그리고 이 암묵적 권한은 읽기에서 끝나지 않습니다. 공개 저장소에 이슈나 PR을 여는 데는 별도 권한이 필요 없습니다 — 아무 GitHub 사용자나 남의 저장소에 이슈를 열 수 있는 것과 똑같이, GitHub App도 자기 설치 토큰으로 아무 공개 저장소에나 이슈와 PR을 만들 수 있습니다. 대상 저장소에 설치돼 있지 않아도 됩니다.
그러면 우회는 이렇게 정리됩니다. 공격자가 자기 앱을 하나 만들고, 자기 저장소에 설치하고(특별한 권한 불필요), 그 설치 토큰으로 대상 공개 저장소에 이슈를 열면 됩니다. 액터가 봇이니 checkWritePermissions는 true를 돌려주고, 워크플로는 공격자가 통제하는 내용을 처리하기 시작합니다.
여기서 뼈아픈 대목은, tag mode에는 액터 타입이 실제 User인지 GitHub API로 확인하는 checkHumanActor 검사가 이미 있었다는 것입니다. agent mode에만 그게 빠져 있었습니다. 방어가 없었던 게 아니라, 두 경로 중 한쪽에만 있었습니다.
이게 첫 번째 교훈입니다. 이름 모양으로 하는 판정은 신뢰 검사가 아닙니다. endsWith("[bot]")은 "이 주체가 신뢰할 만한가"를 묻지 않고 "이 문자열이 어떻게 생겼나"를 묻습니다. 그리고 문자열 모양은 공격자가 고를 수 있습니다.
2단계 — 데이터가 명령이 되는 자리
우회가 되면, 이제 공격자 통제 콘텐츠가 에이전트의 컨텍스트로 들어갑니다. RyotaK가 든 구체적 예는 anthropics/claude-code 저장소의 이슈 트리아지 워크플로였습니다. 이 워크플로는 Claude에게 MCP 도구로 이슈를 읽어오라고 지시하고, 허용 도구 목록에는 이런 것들이 있었습니다.
--allowedTools "Bash(gh label list),
mcp__github__get_issue,
mcp__github__get_issue_comments,
mcp__github__update_issue,
mcp__github__search_issues,
mcp__github__list_issues"
mcp__github__get_issue로 읽고, mcp__github__update_issue로 씁니다. 읽기 도구와 쓰기 도구가 같은 허용 목록에 나란히 있습니다. 이 조합이 나중에 유출 채널이 됩니다.
인젝션 자체의 구조는 이렇습니다. 공격자는 이슈 본문을 에러 메시지처럼 보이게 씁니다. Claude가 이슈를 읽으면, 읽기가 실패한 것처럼 인식하고 "복구"를 시도하면서 본문에 심긴 명령을 실행합니다. 데이터로 읽으라고 준 것이 지시로 해석되는 것 — 인밴드 시그널링의 고전적 실패이고, SQL 인젝션과 XSS가 증명한 바로 그 구조입니다. 다만 여기서 통로는 자연어입니다.
연구자 본인이 각주로 정직하게 달아 둔 대목이 중요합니다 — 그런 설명 하나만으로는 명령 실행이 안정적으로 트리거되지 않고, 실제 익스플로잇은 훨씬 정교한 프롬프트 엔지니어링을 요구했다는 것입니다. 즉 확률적입니다. 그런데 CI는 공격자가 몇 번이고 이슈를 열 수 있는 곳입니다. 확률적 공격에 무제한 재시도가 붙으면 결정론이 됩니다.
3단계 — 읽기 전용처럼 보이는 것들이 유출 채널이다
명령 실행이 되면 다음은 유출입니다. 여기서 두 가지가 겹칩니다.
첫째, Claude Code는 UX를 위해 일부 읽기 전용 명령을 승인 없이 실행하도록 허용해 뒀습니다. RyotaK가 그 직전 글(2026년 1월 12일)에서 파고든 것이 정확히 이 지점입니다. echo, man, sed, sort 같은 명령이 기본 허용이었고, 부작용을 막으려고 인자에 블록리스트를 걸어 뒀는데, 그 블록리스트에 구멍이 8개 있었습니다. 이 건은 CVE-2025-66032를 받았고 Claude Code v1.0.93에서 수정됐습니다(NVD 기준 CVSS 3.1 9.8 / CVSS 4.0 8.7, CWE-77). Anthropic의 대응은 블록리스트를 걷어내고 허용리스트 방식으로 바꾸는 것이었습니다. 연구자의 결론도 같습니다 — 명령 실행 같은 보안 민감 기능에서 블록리스트는 진다.
둘째, 러너 프로세스의 환경 변수에 비밀이 들어 있습니다. 리눅스에는 현재 프로세스의 환경 변수를 그대로 노출하는 의사 파일이 있고, 허용된 읽기 명령으로 그걸 읽으면 비밀이 컨텍스트로 들어옵니다. 그다음 mcp__github__update_issue로 이슈 본문에 되써 주면, 공격자는 그냥 자기 이슈를 새로고침하면 됩니다.
여기서 최종 목표가 왜 OIDC 자격증명이었는지가 중요합니다. GitHub Actions는 워크플로가 자기 신원을 증명하는 OIDC를 지원합니다. 워크플로에 id-token 쓰기 권한이 있으면 GitHub에서 "나는 저장소 Y의 워크플로 X다"라고 서명된 토큰을 받아올 수 있습니다. Claude Code GitHub Actions는 이 메커니즘으로 특권 있는 GitHub App 설치 토큰을 얻습니다 — OIDC 토큰을 받아서 Anthropic 백엔드의 토큰 교환 엔드포인트에 제출하면, 백엔드가 검증하고 Claude GitHub App 설치 토큰을 돌려줍니다.
그 OIDC 토큰을 요청하는 데 필요한 자격증명이 ACTIONS_ID_TOKEN_REQUEST_TOKEN과 ACTIONS_ID_TOKEN_REQUEST_URL이고, 둘 다 환경 변수입니다. 즉 이 두 개가 새면 공격자가 토큰 교환 전 과정을 그대로 재현해서, 저장소 콘텐츠·이슈·PR·워크플로에 쓰기 권한을 가진 설치 토큰을 손에 넣습니다. 저장소가 넘어갑니다.
폭발 반경 — 액션 저장소 자신이 그 워크플로를 쓰고 있었다
여기까지가 "저장소 하나"입니다. 공급망이 되는 이유는 따로 있습니다.
anthropics/claude-code-action — 액션 그 자체의 저장소 — 도 같은 계열의 agent mode 워크플로를 쓰고 있었습니다. 같은 기법으로 액션의 소스에 악성 코드를 심을 수 있었다는 뜻이고, 그러면 그 액션에 의존하는 모든 다운스트림 저장소로 전파됩니다. 도구를 만든 저장소가 그 도구의 취약점에 노출돼 있었던 셈입니다.
RyotaK는 여기에 더해, 액션 없이도 비슷한 수준의 침해에 이르는 흔한 오설정을 하나 더 찾아냈습니다. allowed_non_write_users를 *로 두면 누구나 워크플로를 트리거할 수 있는데, 이게 Anthropic 자신의 저장소와 공식 예제 워크플로에 들어 있었습니다. 예제를 복사한 저장소들이 같은 결함을 그대로 물려받았습니다.
문서는 이 옵션을 위험하다고 표시하면서 "극히 제한된 권한을 가진 워크플로에만" 쓰라고 했고, 예시로 이슈 라벨링을 들었습니다. 그런데 연구자의 반박이 날카롭습니다.
- 이슈 쓰기 권한은 사실 제한적이지 않습니다 — 기존 이슈를 삭제하거나 편집할 수 있습니다.
- 이 워크플로는 어차피 Anthropic API 키를 요구합니다. 그 자체가 민감한 비밀이고, 신뢰할 수 없는 입력에 노출되는 순간 이미 부적합합니다.
- 권한을 조여도 유출은 가능했습니다 — 액션이 완료된 작업 요약을 워크플로 실행 요약 섹션에 기본으로 올리고 있었고, 그 섹션은 공개입니다.
그리고 이 둘을 엮으면 권한 상승이 됩니다. 느슨한 트리아지 워크플로로 이슈 쓰기 토큰을 얻고(요약 섹션을 통해), 그 토큰으로 신뢰된 사용자가 올린 이슈를 트리거 이후에 편집해서, 엄격한 tag mode 워크플로가 그걸 신뢰된 맥락으로 처리하게 만듭니다. tag mode는 id-token 쓰기 권한을 갖고 있으니 거기서 OIDC 자격증명이 나옵니다. 외부 사용자에서 저장소 완전 장악까지, 두 워크플로의 합성으로 도달합니다.
각 워크플로를 따로 보면 둘 다 "의도대로" 동작합니다. 결함은 합성에서 나옵니다.
이건 가설이 아니다 — Cline에서 실제로 터졌다
여기까지는 책임 있는 공개였고, 실제 피해는 없었습니다(연구자는 자기 테스트 저장소에서만 검증했다고 각주로 밝혔습니다). 그런데 같은 결함 계열이 다른 제품에서 실제로 악용됐습니다.
2026년 2월 9일, 보안 연구자 Adnan Khan이 Clinejection을 공개했습니다. AI 코딩 도구 Cline의 이슈 트리아지 워크플로 — 역시 claude-code-action 기반이고, Bash·Write·Edit 도구가 열려 있었으며, 아무 GitHub 사용자나 이슈를 열어 트리거할 수 있었습니다 — 에 프롬프트 인젝션이 가능했습니다. 취약 기간은 2025년 12월 21일부터 2026년 2월 9일까지였습니다.
체인이 흥미롭습니다. 트리아지 워크플로에서 곧장 비밀을 빼는 게 아니라, GitHub Actions 캐시를 오염시켜 나이틀리 릴리스·npm 배포 워크플로로 건너뛰고 거기서 배포 자격증명을 훔칩니다. 그리고 그 나이틀리 자격증명이 프로덕션 자격증명과 같은 권한을 갖습니다 — VS Code 마켓플레이스와 OpenVSX는 토큰을 확장이 아니라 퍼블리셔에 묶고, npm은 프로덕션과 나이틀리가 같은 패키지를 씁니다. 나이틀리를 낮은 등급 자격증명이라고 생각했지만, 레지스트리의 자격증명 모델이 그 구분을 지원하지 않았습니다.
공개 과정도 기록해 둘 만합니다. Khan은 2026년 1월 1일에 GitHub 비공개 취약점 신고와 이메일로 알렸고, 1월 8일 후속 이메일, 1월 18일 CEO에게 DM, 2월 7일 마지막 시도까지 응답을 받지 못했습니다. 2월 9일 공개 후 1시간이 안 되어 수정됐습니다(PR 9211 — AI 워크플로 제거, 나이틀리 잡의 캐시 사용 중단). Cline은 2월 10일 공식 확인과 함께 즉시 완화 조치를 알렸고, 2월 11일에는 전체 자격증명 로테이션과 감사 결과 — 2025년 12월 21일부터 2026년 2월 9일 사이 무단 릴리스는 없었고 npm 41개 버전이 모두 소스와 일치 — 를 전했습니다.
그런데 그 감사 창은 2월 9일에서 끝납니다. 2026년 2월 17일, 침해된 npm 배포 토큰으로 [email protected]이 무단 배포됐습니다(GHSA-9ppg-jx86-fqw7). 정상 버전 2.2.3과의 차이는 package.json에 postinstall 스크립트 한 줄이 추가된 것뿐이었고, 그 스크립트는 무관한 패키지 하나를 전역 설치했습니다. 악성 버전은 태평양 시간 기준 오전 3시 26분부터 11시 30분까지 약 8시간 살아 있었습니다(SafeDep 분석, 2026년 2월 18일).
여기서 정직하게 표시해 둘 것들이 있습니다.
- 설치된 패키지는 합법적이고 악성이 아닙니다. 자격증명을 훔치거나 백도어를 심지 않았습니다. SafeDep의 판단은 개념 증명에 가깝다는 것입니다 — 실제 페이로드가 아니라 실현 가능성을 보여준 것. GHSA의 심각도 등급도 Low입니다.
- 다만 같은 postinstall 훅으로 자격증명 탈취기나 리버스 셸을 똑같이 배달할 수 있었습니다. 페이로드는 온순했지만 메커니즘은 증명됐습니다.
- 토큰 탈취의 정확한 경로는 공개적으로 확인되지 않았습니다. SafeDep이 그렇게 명시합니다. Khan 본인은 자기 PoC를 대상 저장소가 아니라 미러에서 돌렸고, 다른 행위자가 그 PoC를 찾아 Cline을 직접 공격해 자격증명을 얻었다고 업데이트에 적었습니다.
- 영향받은 설치 수의 구체적 숫자는 GHSA에 없습니다. 여기서 지어내지 않겠습니다.
교훈은 그래도 선명합니다. 로테이션은 창을 닫았다고 믿는 순간 끝나지 않습니다. 2월 9일까지를 감사하고 깨끗하다고 확인한 조직에서, 2월 17일에 배포가 나갔습니다. Cline은 이후 토큰을 폐기하고 npm 배포를 OIDC provenance로 옮겨, 장기 정적 토큰이라는 공격면 자체를 없앴습니다. 그게 진짜 수정입니다 — 토큰을 다시 발급하는 게 아니라 토큰을 없애는 것.
같은 제품, 다른 문 — 인젝션이 아니어도 뚫립니다
한 가지를 덧붙여야 공정합니다. 같은 액션에는 프롬프트 인젝션이 아닌 경로로 같은 결과에 도달한 취약점도 따로 공개돼 있습니다. GHSA-8q5r-mmjf-575q / CVE-2026-47751(2026년 5월 20일 공개, 심각도 medium, 1.0.74 미만 영향, CWE-78과 CWE-200, HackerOne의 reptou 제보)입니다.
권고문이 설명하는 원인은 세 가지의 조합입니다 — PR의 head 브랜치를 체크아웃하고(공격자 통제 콘텐츠), 기본 설정 소스를 통해 작업 디렉터리의 .mcp.json을 읽고, 프로젝트 MCP 서버를 전부 무조건 활성화하는 설정이 켜져 있었던 것. 그 결과 악성 .mcp.json이 담긴 PR을 연 공격자가 러너에서 임의 코드를 실행할 수 있었고, 특권 사용자가 그 PR에서 액션을 트리거하면 워크플로가 가진 비밀(API 키와 토큰)이 유출될 수 있었습니다.
여기서 모델을 속인 건 아무것도 없습니다. 에이전트의 설정 자체가 공격자 통제 하에 있었습니다. 그리고 원인의 형태가 앞의 사건과 똑같습니다 — 세 가지 결정이 각각 따로 보면 다 합리적인데(PR 체크아웃은 리뷰하려면 당연하고, 설정 파일을 읽는 것도, 프로젝트 MCP 서버를 켜는 것도), 셋이 만나는 자리에서 결함이 생깁니다.
이게 세 번째 교훈입니다. 인젝션 방어에만 집중하면 이 계열을 통째로 놓칩니다. 신뢰할 수 없는 저장소 콘텐츠는 프롬프트만이 아니라 설정·의존성·빌드 스크립트로도 들어옵니다. 에이전트가 읽는 모든 것이 입력이고, 설정 파일은 그중 가장 강력한 입력입니다.
Anthropic이 실제로 넣은 방어
RyotaK는 Anthropic이 매우 신속했다고 평가합니다. 타임라인은 이렇습니다 — 2026년 1월 12일 권한 우회 신고, 1월 16일 수정(나흘), 1월 17일 오설정 신고, 2월부터 4월까지 여러 차례의 추가 완화와 후속 우회, 6월 1일 공개. 오설정 건은 Claude Code GitHub Actions v1.0.94에서 정리됐습니다. Anthropic은 이 건들을 CVSS v4.0 기준 7.8로 평가하고 바운티를 지급했습니다(3,800달러에 우회 보너스 1,000달러 추가). 연구자의 글에는 이 건에 대한 CVE나 GHSA 번호가 나오지 않고, 필자도 찾지 못했습니다 — 뒤에 나오는 CVE-2026-47751은 같은 액션의 다른 결함입니다.
넣은 방어를 성격별로 나눠 보면 이렇습니다.
진짜 경계를 고친 것. GitHub App이 기본적으로 워크플로를 트리거하지 못하게 막았습니다(수정 커밋은 agent mode의 prepare 단계에 checkHumanActor 호출을 추가합니다). 그리고 트리거 이후 편집된 이슈·코멘트를 처리하지 않게 해서 워크플로 합성 공격을 끊었습니다. 이 둘은 신뢰 경계 자체를 옮긴 수정입니다.
채널을 닫은 것. 워크플로 실행 요약 섹션을 기본 비활성화해 공개 유출 통로 하나를 없앴습니다.
속도를 늦춘 것. 나머지는 여기에 속합니다. Claude Code가 낳는 자식 프로세스에서 환경 변수를 스크럽하고, gh 명령에 커스텀 래퍼를 씌워 인자를 검증합니다. 이 래퍼가 왜 필요했는지가 이 글에서 가장 값진 대목입니다 — gh issue view는 이슈를 읽는, 누가 봐도 읽기 전용인 명령입니다. 그런데 gh CLI는 위치 인자로 URL을 받습니다. 즉 인젝션이 비밀 값을 URL에 실어 보내게 만들면, "읽기 전용" 명령이 그대로 유출 채널이 됩니다. 래퍼는 이슈 번호가 숫자 하나인지 검사해서 이걸 막습니다.
이게 두 번째 교훈입니다. 도구의 유출 능력은 이름이 아니라 인자 표면에서 나옵니다. 임의의 URL이나 임의의 경로를 받을 수 있는 도구는, 그 이름이 아무리 read/list/view여도 외부 통신 수단입니다. 허용 목록을 도구 이름 단위로 짜면 이걸 놓칩니다.
정직한 한계 — 벤더 문서가 스스로 적어 둔 것
여기서부터가 이 글을 쓴 이유입니다. 보통 벤더 문서는 방어를 과장하는데, claude-code-action의 보안 문서는 그러지 않습니다. 그래서 인용할 값이 있습니다.
환경 변수 스크럽에 대해 문서는 이렇게 적습니다 — 최선 노력(best-effort) 스크럽이고, 리눅스 러너에서 bubblewrap이 있으면 PID 네임스페이스 격리가 추가되며, 그러나 "이것은 프롬프트 인젝션 위험을 줄이지만 없애지는 못한다". 그러니 워크플로 권한을 최소로 유지하고 모든 출력을 검증하라고 합니다.
정적 토큰에 대해서는 더 구체적입니다 — 개인 액세스 토큰을 쓰지 말라, 정적 토큰은 실행 간에 로테이트되지 않아서 "프롬프트 인젝션을 통해 시간이 지나며 부분적으로 또는 완전히 복구될 수 있다". 그리고 이어지는 문장이 압권입니다 — 허용 도구를 제한하면 "복구 속도를 줄이지만 위험을 제거하지는 못할 수 있다".
이 표현을 그냥 넘기면 안 됩니다. 방어가 유출을 막는다고 하지 않고, 유출 속도를 늦춘다고 말하고 있습니다. 즉 모델이 이건 이진 경계가 아니라 누수율입니다. 토큰이 충분히 오래 살아 있고 공격자가 충분히 여러 번 시도하면, 결국 다 샙니다. 그래서 처방이 "토큰을 지키라"가 아니라 "수명 짧은 토큰을 쓰라"인 것입니다.
콘텐츠 위생 처리도 마찬가지입니다. 액션은 HTML 주석, 비가시 문자, 마크다운 이미지 alt 텍스트, 숨겨진 HTML 속성, HTML 엔티티를 걷어냅니다. 그리고 문서는 바로 덧붙입니다 — "새로운 우회 기법이 나타날 수 있다". 그래서 외부 기여자에게서 오는 입력은 Claude가 처리하기 전에 원본을 검토하라고 권합니다.
봇 차단에도 잔여 위험이 문서화돼 있습니다. allowed_bots로 허용한 봇은 저장소 권한을 검사하지 않습니다. 공개 저장소에서는 누구나 만든 GitHub App을 포함한 외부 주체가 이슈 열기·코멘트·PR 리뷰 같은 이벤트를 일으킬 수 있고, 워크플로가 그 이벤트를 듣고 있는데 allowed_bots가 *이면, 그런 앱이 자기가 통제하는 프롬프트로 액션을 호출할 수 있습니다. 즉 원래 취약점이 기본값에서는 닫혔지만, 옵션 하나로 다시 열립니다.
그리고 연구자 본인의 각주가 이 모든 걸 한 문장으로 요약합니다. Claude Code는 환경 변수 의사 파일에 대한 단순한 읽기를 완화하지만, 그 완화는 "허들을 높이는 심층 방어이지 보안 경계가 아니며, 더 정교한 유출 기법은 여전히 가능하다".
측정된 한계도 있습니다. RyotaK는 글을 쓰는 시점에 Claude Code의 권한 시스템을 우회해 임의 명령을 실행하는 취약점을 약 50건 Anthropic에 신고했다고 밝힙니다. 그의 결론은 완곡하지 않습니다 — Claude Code 주변의 권한 모델은 신뢰할 수 없는 입력을 안전하게 다룰 만큼 단단하지 않다. 그래서 그는 권한이 아무리 좁아도 allowed_non_write_users를 아예 쓰지 말라고 권합니다.
이 숫자를 정확히 읽어야 합니다. 50건은 "Claude Code가 유난히 허술하다"는 뜻이 아닙니다. 활발히 연구되고 신속히 고쳐지는 제품에서, 한 연구자가 혼자서 그만큼을 찾았다는 뜻입니다. 명령 파서 위에 얹은 권한 시스템은 파서가 완벽해야 성립하는데, 셸 명령 파싱은 완벽해지지 않는 종류의 문제입니다. Willison의 문장이 여전히 유효합니다 — 우리는 아직 이걸 100% 신뢰성 있게 막는 법을 모릅니다.
그래서 에이전트를 만드는 사람은 무엇을 해야 하나
이 사건에서 실제로 이월되는 설계 원칙만 추리면 이렇습니다.
1. 신뢰는 주체의 모양이 아니라 검증된 속성으로 판정합니다. 문자열 접미사, 사용자명 패턴, 헤더 값 같은 건 공격자가 고를 수 있습니다. checkHumanActor처럼 발급처에 물어보는 검사가 진짜입니다. 그리고 그 검사는 트리거 경로 전부에 있어야 합니다 — 한 경로에만 있는 방어는 없는 방어입니다.
2. 신뢰할 수 없는 입력을 처리하는 워크플로에는 비밀을 주지 않습니다. 이게 가장 확실한 수입니다. 인젝션을 못 막는다고 가정하고, 샐 게 없는 상태를 만드는 것. Anthropic API 키조차 민감하다는 지적이 여기서 나옵니다.
3. 도구 허용 목록은 이름이 아니라 인자 표면으로 짭니다. gh issue view가 유출 채널이 될 수 있다는 사실이 이 원칙을 증명합니다. 임의 URL·임의 경로·임의 명령을 받는 인자가 있으면 그건 외부 통신 수단입니다. Khan이 Cline에 권한 것도 같습니다 — 트리아지 워크플로에 Bash·Write·Edit를 주지 마라.
4. 워크플로는 개별이 아니라 합성으로 검토합니다. 느슨한 워크플로와 엄격한 워크플로가 한 저장소에 있으면, 공격자는 둘을 이어 붙입니다. 캐시, 이슈 상태, 아티팩트 — 워크플로 사이의 모든 공유 상태가 신뢰 경계입니다. Khan의 권고 하나가 이걸 정확히 짚습니다 — 프로덕션 배포 비밀을 가진 워크플로에서는 캐시를 소비하지 마라. 릴리스 빌드에서는 몇 분 아끼는 것보다 무결성이 중요합니다.
5. 비프로덕션 자격증명이 프로덕션 자격증명인지 확인합니다. Cline 건의 핵심이 여기였습니다. 레지스트리가 토큰을 퍼블리셔에 묶는다면, 나이틀리 토큰은 프로덕션 토큰입니다. 네임스페이스를 분리하거나, 아예 정적 토큰을 없애고 OIDC provenance로 가야 합니다.
6. 방어를 누수율로 모델링합니다. 벤더 문서가 "복구 속도를 줄인다"고 쓴 걸 진지하게 받아들이면, 설계가 바뀝니다. 심층 방어는 공격 비용을 올리는 것이지 공격을 불가능하게 만드는 게 아닙니다. 그러니 짝이 되는 대책은 수명 단축과 폭발 반경 축소입니다 — 수명 짧은 토큰, 범위 좁은 권한, 그리고 로그. RyotaK가 독자에게 워크플로 실행 로그에서 침해 흔적을 확인하라고 권한 것도 같은 이유입니다.
마치며
이 사건이 특별한 건 새로운 공격 기법 때문이 아닙니다. 오히려 전부 오래된 것들입니다 — 신뢰 검사 결함, 인밴드 시그널링, 블록리스트의 패배, 자격증명 범위 오해, 워크플로 합성. 새로운 건 이것들을 하나로 꿰는 실이 자연어라는 점입니다. 이슈 본문이 명령이 되는 순간, 이슈를 열 수 있는 사람은 전부 잠재적 CI 실행 권한자가 됩니다.
Anthropic의 대응은 빠르고 성실했습니다(나흘 만의 수정, 문서화된 잔여 위험, 지급된 바운티). Cline의 대응은 느렸고, 그 지연은 실제 무단 배포로 이어졌습니다. 두 대비가 말해 주는 건 프로세스의 중요성이지, 어느 쪽이 인젝션을 풀었다는 게 아닙니다. 아무도 풀지 못했습니다. 벤더 문서가 그렇게 적고 있고, 연구자가 50건으로 그걸 증명했고, 그 사이 실제 침해가 한 건 났습니다.
그러니 지금 CI에 에이전트를 붙이고 있다면, 질문을 바꾸는 게 낫습니다. "인젝션을 어떻게 막지"가 아니라 "인젝션이 성공한다고 가정하면, 이 러너에서 무엇이 샐 수 있지" 입니다. 두 번째 질문에는 답할 수 있습니다. 첫 번째 질문에는 2026년 7월 현재 아무도 답하지 못합니다.
참고 자료
- Poisoning Claude Code: One GitHub Issue to Break the Supply Chain — RyotaK, GMO Flatt Security (2026-06-01)
- Pwning Claude Code in 8 Different Ways — RyotaK, GMO Flatt Security (2026-01-12)
- CVE-2025-66032 — NVD 항목(Claude Code v1.0.93 이전 읽기 전용 검증 우회)
- claude-code-action 보안 문서 — 잔여 위험과 한계가 명시된 벤더 1차 자료
- agent mode에 checkHumanActor를 추가한 수정 커밋
- GHSA-8q5r-mmjf-575q / CVE-2026-47751 — 악성 .mcp.json을 통한 RCE·비밀 유출(같은 액션의 다른 결함)
- Clinejection — Adnan Khan (2026-02-09)
- GHSA-9ppg-jx86-fqw7 — cline 2.3.0 무단 배포 권고
- AI Agent Cline v2.3.0 Compromised — SafeDep (2026-02-18)
- The lethal trifecta for AI agents — Simon Willison (2025-06-16)
- 웹 에이전트의 교차 사이트 프롬프트 인젝션과 Prismata의 봉쇄(관련 글)
현재 단락 (1/95)
2026년 6월 1일, GMO Flatt Security의 RyotaK가 [Poisoning Claude Code: One GitHub Issue to Break the Suppl...