Skip to content

Split View: 이슈 하나로 공급망 끝까지 — CI 안의 에이전트가 무너진 방식과, 방어가 실제로 막아준 만큼

|

이슈 하나로 공급망 끝까지 — CI 안의 에이전트가 무너진 방식과, 방어가 실제로 막아준 만큼

들어가며 — 이슈 본문이 명령이 되는 지점

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_TOKENACTIONS_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 배포 토큰으로 cline@2.3.0이 무단 배포됐습니다(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월 현재 아무도 답하지 못합니다.

참고 자료

One Issue, the Whole Supply Chain — How an Agent Inside CI Broke, and What the Defenses Actually Bought

Introduction — Where an Issue Body Becomes a Command

On June 1, 2026, RyotaK of GMO Flatt Security disclosed Poisoning Claude Code: One GitHub Issue to Break the Supply Chain. The gist is exactly what the title says — a single GitHub issue could take over a repository that used Claude Code GitHub Actions, and the affected repositories included Anthropic's own.

A previous post covered cross-site prompt injection in web agents. Where that post was a defense design grounded in a paper, this one is something that actually happened in a shipped product, with the timeline, CVSS scores, bounty amounts, and fix commits all public. And there's one more important difference — the same flaw family was actually exploited in a different product.

This post is not an attack recipe. It doesn't carry a payload. Instead it looks only at what an engineer building agents actually needs — exactly where the trust boundary broke, what the vendor's defenses do and don't stop, and whether there's a point where you can say "that's enough."

The Stage — What the Agent Inside CI Was Holding

Claude Code GitHub Actions is a workflow that hooks Claude Code into CI/CD. It handles things like automated code review, issue triage and labeling, and comment-driven code generation. It has two operating modes.

  • tag mode — triggers when a specific keyword (a Claude mention, by default) is mentioned in an issue or PR comment.
  • agent mode — triggers when a workflow has a prompt input configured. Used for slash commands or scheduled jobs.

To do anything with GitHub, the Claude GitHub App has to be installed on the repository, and the permissions that app holds are: read/write repository contents (code), read/write issues and PRs, read/write discussions, read/write workflows (Actions). Unless a separate token is specified, this app's token is used by default. Setup gets easier, and the attack surface gets wider.

This is already enough to assemble what Simon Willison called the lethal trifecta — access to private data, exposure to untrusted content, and a means of communicating externally. When all three land in one process, a single injection turns into exfiltration. A CI runner has, textbook-style, all three.

Step 1 — The One Line That Said "If It's a Bot, Let It Through"

Aware of this risk, the action by default blocked users without write permission from triggering workflows. The documentation states it plainly — the action can only be triggered by users with write access to the repository.

This control was implemented in the checkWritePermissions function. The function checks whether the actor's permission is write or admin. But ahead of that check sat a branch like this.

// src/github/validation/permissions.ts (before the fix)
if (actor.endsWith("[bot]")) {
  core.info(`Actor is a GitHub App: ${actor}`);
  return true;
}

If the actor's name ends in [bot], it passes unconditionally, regardless of actual permissions. On the surface this looks reasonable — a GitHub App is usually a trusted entity installed by the repository's admin. The problem is that assumption doesn't hold on public repositories.

As GitHub's own documentation states, a GitHub App has an implicit permission to read public resources even with no default permissions. And that implicit permission doesn't stop at reading. Opening an issue or PR on a public repository requires no special permission — just as any GitHub user can open an issue on someone else's repository, a GitHub App can use its own installation token to create issues and PRs on any public repository. It doesn't need to be installed on the target repository.

So the bypass comes together like this. An attacker creates their own app, installs it on their own repository (no special permission required), and uses that installation token to open an issue on the target public repository. Since the actor is a bot, checkWritePermissions returns true, and the workflow starts processing content the attacker controls.

What stings here is that a checkHumanActor check — which asks the GitHub API whether the actor type is really a User — already existed in tag mode. It was only agent mode that was missing it. The defense wasn't absent; it existed on only one of the two paths.

This is the first lesson. Judging trust by the shape of a name is not a trust check. endsWith("[bot]") doesn't ask "is this actor trustworthy" — it asks "what does this string look like." And the shape of a string is something an attacker gets to choose.

Step 2 — Where Data Becomes a Command

Once the bypass works, attacker-controlled content flows into the agent's context. The concrete example RyotaK gives is the issue-triage workflow in the anthropics/claude-code repository. This workflow instructs Claude to read issues via an MCP tool, and its allowed-tool list included things like this.

--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 reads, mcp__github__update_issue writes. Read and write tools sit side by side on the same allow list. That combination is what later becomes the exfiltration channel.

The structure of the injection itself is this. The attacker writes the issue body to look like an error message. When Claude reads the issue, it perceives the read as having failed and, while attempting to "recover," executes the command planted in the body. Something handed over to be read as data is instead interpreted as an instruction — the classic failure of in-band signaling, the same structure SQL injection and XSS have already proven out. Here, though, the channel is natural language.

Something the researcher himself honestly notes in a footnote matters — that description alone doesn't reliably trigger command execution; the actual exploit required far more sophisticated prompt engineering. In other words, it's probabilistic. But CI is a place where an attacker can open an issue as many times as they like. Attach unlimited retries to a probabilistic attack, and it becomes deterministic.

Step 3 — What Looks Read-Only Is the Exfiltration Channel

Once you have command execution, exfiltration comes next. Two things overlap here.

First, for the sake of UX, Claude Code was set up to run some read-only commands without approval. This is exactly the spot RyotaK dug into in the post right before this one (January 12, 2026). Commands like echo, man, sed, and sort were allowed by default, with a blocklist on arguments meant to prevent side effects — and that blocklist had eight holes in it. This was assigned CVE-2025-66032 and fixed in Claude Code v1.0.93 (per NVD, CVSS 3.1 9.8 / CVSS 4.0 8.7, CWE-77). Anthropic's response was to rip out the blocklist and switch to an allowlist approach. The researcher's conclusion is the same — blocklists lose in security-sensitive functionality like command execution.

Second, the runner process's environment variables hold secrets. Linux has a pseudo-file that exposes the current process's environment variables as-is, and reading it with an allowed read command pulls secrets into the context. Then, writing that back into the issue body via mcp__github__update_issue, the attacker just has to refresh their own issue.

This is where it matters why the ultimate target was OIDC credentials. GitHub Actions supports OIDC, which lets a workflow prove its own identity. If a workflow has id-token write permission, it can obtain a signed token from GitHub saying "I am workflow X in repository Y." Claude Code GitHub Actions uses this mechanism to obtain a privileged GitHub App installation token — it gets an OIDC token, submits it to Anthropic's backend token-exchange endpoint, the backend verifies it, and returns a Claude GitHub App installation token.

The credentials needed to request that OIDC token are ACTIONS_ID_TOKEN_REQUEST_TOKEN and ACTIONS_ID_TOKEN_REQUEST_URL, both environment variables. In other words, if these two leak, an attacker can reproduce the entire token-exchange process and end up with an installation token that has write access to repository contents, issues, PRs, and workflows. The repository is handed over.

Blast Radius — The Action's Own Repository Used That Same Workflow

So far this is "one repository." The reason this becomes a supply-chain issue is separate.

anthropics/claude-code-action — the repository for the action itself — was also running the same family of agent-mode workflow. That means the same technique could plant malicious code in the action's own source, which then propagates to every downstream repository that depends on it. The repository that built the tool was itself exposed to that tool's vulnerability.

RyotaK also found one more common misconfiguration that reaches a comparable level of compromise even without the action's own bug. Setting allowed_non_write_users to * lets anyone trigger the workflow, and this was present both in Anthropic's own repository and in the official example workflows. Repositories that copied the example inherited the same flaw wholesale.

The documentation flags this option as dangerous and says to use it "only for workflows with extremely limited permissions," citing issue labeling as an example. But the researcher's rebuttal is sharp.

  • Issue write permission isn't actually limited — it can delete or edit existing issues.
  • This workflow requires an Anthropic API key regardless. That's a sensitive secret on its own, and exposing it to untrusted input is already unfit for purpose.
  • Even with tightened permissions, exfiltration was still possible — the action posts a summary of the completed work to the workflow run's summary section by default, and that section is public.

And chaining these two becomes privilege escalation. Get an issue-write token from the loose triage workflow (via the summary section), use that token to edit an issue posted by a trusted user after it's been triggered, and get the strict tag-mode workflow to process it as trusted context. Tag mode holds id-token write permission, which is where the OIDC credentials come from. From an external user to full repository takeover — reached by composing two workflows.

Looked at individually, each workflow behaves "as intended." The flaw comes from composition.

This Isn't Hypothetical — It Actually Happened to Cline

Up to here, this was responsible disclosure and no real damage occurred (the researcher notes in a footnote that testing was confined to his own test repositories). But the same flaw family was actually exploited in a different product.

On February 9, 2026, security researcher Adnan Khan disclosed Clinejection. The AI coding tool Cline's issue-triage workflow — also built on claude-code-action, with Bash, Write, and Edit tools open, and triggerable by any GitHub user opening an issue — was vulnerable to prompt injection. The vulnerable window ran from December 21, 2025 to February 9, 2026.

The chain is interesting. Rather than pulling secrets straight out of the triage workflow, it instead poisons the GitHub Actions cache to jump over into the nightly-release and npm-publish workflows and steal deployment credentials there. And those nightly credentials carry the same permissions as production credentials — the VS Code Marketplace and OpenVSX bind tokens to the publisher, not the extension, and npm uses the same package for both production and nightly. Nightly was assumed to be a lower-tier credential, but the registries' credential model didn't support that distinction.

The disclosure timeline is worth recording too. Khan reported it via a GitHub private vulnerability report and email on January 1, 2026, followed by a follow-up email on January 8, a DM to the CEO on January 18, and a final attempt on February 7 — with no response through all of it. It was fixed in under an hour after disclosure on February 9 (PR 9211 — removed the AI workflow, stopped the nightly job's cache use). Cline officially confirmed and announced immediate mitigation on February 10, and on February 11 reported a full credential rotation and audit results — no unauthorized releases between December 21, 2025 and February 9, 2026, and all 41 npm versions matched their source.

But that audit window ends on February 9. On February 17, 2026, using a compromised npm publish token, cline@2.3.0 was published without authorization (GHSA-9ppg-jx86-fqw7). The only difference from the legitimate 2.2.3 version was one added postinstall script line in package.json, and that script globally installed one unrelated package. The malicious version was live for roughly 8 hours, from 3:26 AM to 11:30 AM Pacific time (SafeDep's analysis, February 18, 2026).

A few things worth flagging honestly here.

  • The installed package was legitimate and not malicious. It didn't steal credentials or plant a backdoor. SafeDep's assessment is that this is closer to a proof of concept — demonstrating feasibility rather than delivering a real payload. GHSA's severity rating is also Low.
  • Still, the same postinstall hook could have just as easily delivered a credential stealer or a reverse shell. The payload was benign, but the mechanism was proven.
  • The exact path of the token theft has not been publicly confirmed. SafeDep says so explicitly. Khan himself notes in his update that he ran his PoC against a mirror rather than the target repository, and that a different actor apparently found that PoC and attacked Cline directly to obtain the credentials.
  • GHSA gives no specific number for how many installs were affected. I won't invent one here.

The lesson is still clear, though. Rotation doesn't end the moment you believe the window is closed. An organization that audited through February 9 and confirmed it was clean saw a release go out on February 17. Cline subsequently revoked the tokens and moved npm publishing to OIDC provenance, removing the long-lived static token as an attack surface entirely. That's the real fix — not reissuing the token, but eliminating it.

Same Product, Different Door — You Don't Need Injection to Break In

To be fair, one more thing should be added. The same action also has a separately disclosed vulnerability that reaches the same outcome through a path that is not prompt injection. GHSA-8q5r-mmjf-575q / CVE-2026-47751 (disclosed May 20, 2026, medium severity, affects versions below 1.0.74, CWE-78 and CWE-200, reported by reptou via HackerOne).

The advisory describes a combination of three causes — checking out the PR's head branch (attacker-controlled content), reading the working directory's .mcp.json through the default config source, and having a setting enabled that unconditionally activates every project MCP server. As a result, an attacker who opened a PR containing a malicious .mcp.json could execute arbitrary code on the runner, and if a privileged user triggered the action on that PR, the workflow's secrets (API keys and tokens) could leak.

No model was tricked here at all. The agent's configuration itself was under attacker control. And the shape of the cause is identical to the earlier incident — three decisions that each look reasonable in isolation (checking out a PR to review it, reading a config file, turning on project MCP servers), where the flaw appears at the point where all three meet.

This is the third lesson. Focusing only on injection defenses misses this entire family. Untrusted repository content doesn't arrive only as a prompt — it also arrives as configuration, dependencies, and build scripts. Everything the agent reads is input, and the configuration file is the most powerful input among them.

The Defenses Anthropic Actually Shipped

RyotaK rates Anthropic's response as very fast. The timeline runs like this — the permission bypass reported January 12, 2026, fixed on January 16 (four days), the misconfiguration reported January 17, several rounds of additional mitigation and follow-up bypasses from February through April, disclosure on June 1. The misconfiguration issue was cleaned up in Claude Code GitHub Actions v1.0.94. Anthropic rated these at 7.8 on CVSS v4.0 and paid a bounty (3,800plusa3,800 plus a 1,000 bypass bonus). The researcher's post gives no CVE or GHSA number for this issue, and I could not find one either — the CVE-2026-47751 mentioned later is a different flaw in the same action.

Sorting the shipped defenses by character:

Fixes that moved a real boundary. The GitHub App was blocked from triggering workflows by default (the fix commit adds a checkHumanActor call to the prepare step of agent mode). And issues or comments edited after triggering are no longer processed, cutting off the workflow-composition attack. Both of these are fixes that moved the trust boundary itself.

Closures that shut a channel. The workflow run's summary section was disabled by default, removing one public exfiltration route.

Slowdowns. Everything else falls here. Environment variables are scrubbed from child processes spawned by Claude Code, and a custom wrapper around the gh command validates arguments. Why that wrapper was necessary is the most valuable detail in this whole story — gh issue view is, by any reasonable read, a read-only command for reading an issue. But the gh CLI accepts a URL as a positional argument. So if an injection makes it send a secret value as part of that URL, a "read-only" command becomes an exfiltration channel as-is. The wrapper blocks this by checking that the issue number is a single number.

This is the second lesson. A tool's exfiltration capability comes from its argument surface, not its name. A tool that can accept an arbitrary URL or an arbitrary path is a means of external communication no matter how read/list/view-ish its name sounds. Writing allow lists at the level of tool names misses this.

The Honest Limits — What the Vendor's Own Documentation Admits

This is where the real reason for writing this post begins. Vendor documentation usually oversells its defenses, but claude-code-action's security documentation doesn't. So there's something worth quoting.

On environment-variable scrubbing, the documentation states this — it's a best-effort scrub, PID namespace isolation is added on Linux runners where bubblewrap is available, but "this reduces but does not eliminate prompt injection risk." So it recommends keeping workflow permissions minimal and validating all output.

On static tokens, it's even more specific — don't use personal access tokens; static tokens don't rotate between runs, so they "can be partially or fully recovered over time through prompt injection." And the sentence that follows is the sharpest part — restricting allowed tools "reduces the speed of recovery but may not eliminate the risk."

This phrasing shouldn't be skimmed past. It doesn't say the defense stops exfiltration — it says it slows the rate of exfiltration. In other words, the model here is not a binary boundary but a leak rate. If a token lives long enough and an attacker tries enough times, eventually it all leaks out. That's why the prescription isn't "protect the token" but "use short-lived tokens."

Content sanitization is the same story. The action strips HTML comments, invisible characters, markdown image alt text, hidden HTML attributes, and HTML entities. And the documentation immediately adds — "new bypass techniques may emerge." So it recommends reviewing raw input from external contributors before Claude processes it.

Residual risk is documented for bot blocking too. Bots allowed via allowed_bots do not have their repository permissions checked. On a public repository, an external entity — including any GitHub App anyone made — can trigger events like opening an issue, commenting, or reviewing a PR, and if a workflow is listening for those events with allowed_bots set to *, that app can invoke the action with a prompt it controls. In other words, the original vulnerability is closed by default, but reopened by a single option.

And the researcher's own footnote sums all of this up in one sentence. Claude Code mitigates simple reads of the environment-variable pseudo-file, but that mitigation is "a defense-in-depth measure that raises the bar, not a security boundary, and more sophisticated exfiltration techniques remain possible."

There's a measured limit here too. RyotaK states that, as of writing, he had reported roughly 50 vulnerabilities that bypass Claude Code's permission system to execute arbitrary commands to Anthropic. His conclusion is not diplomatic — the permission model around Claude Code is not robust enough to safely handle untrusted input. So he recommends not using allowed_non_write_users at all, no matter how narrow the permission.

This number needs to be read correctly. 50 doesn't mean "Claude Code is unusually sloppy." It means one researcher, alone, found that many in a product that is actively researched and quickly patched. A permission system layered on top of a command parser only holds if the parser is perfect, and shell command parsing is the kind of problem that never becomes perfect. Willison's line still holds — we still don't know how to block this with 100% reliability.

So What Should Agent Builders Actually Do

Stripped down to the design principles that actually carry over from this incident:

1. Judge trust by verified properties, not by the shape of the actor. String suffixes, username patterns, header values — an attacker gets to pick all of these. A check that asks the issuer, like checkHumanActor, is the real thing. And that check needs to be present on every trigger path — a defense that exists on only one path is no defense at all.

2. Don't hand secrets to a workflow that processes untrusted input. This is the surest move. Assume you can't stop the injection, and instead build a state where there's nothing left to leak. This is where the point about even an Anthropic API key being sensitive comes from.

3. Build tool allow lists around the argument surface, not the name. The fact that gh issue view can become an exfiltration channel proves this principle. A tool with an argument that accepts an arbitrary URL, arbitrary path, or arbitrary command is a means of external communication. What Khan recommends for Cline is the same — don't give Bash, Write, and Edit to a triage workflow.

4. Review workflows by composition, not individually. If a loose workflow and a strict workflow live in the same repository, an attacker will chain them together. Caches, issue state, artifacts — every piece of state shared between workflows is a trust boundary. One of Khan's recommendations nails this exactly — don't consume a cache in a workflow that holds production deployment secrets. Integrity matters more than saving a few minutes on a release build.

5. Verify whether your non-production credential is actually a production credential. This was the crux of the Cline incident. If a registry binds tokens to the publisher, a nightly token is a production token. Either separate the namespaces, or drop static tokens altogether and move to OIDC provenance.

6. Model your defenses as a leak rate. Taking the vendor documentation's phrase "reduces the speed of recovery" seriously changes the design. Defense in depth raises the cost of an attack; it doesn't make the attack impossible. So the matching countermeasure is shortening lifetimes and shrinking blast radius — short-lived tokens, narrowly scoped permissions, and logging. This is the same reason RyotaK recommends readers check workflow run logs for signs of compromise.

Closing

What makes this incident notable isn't a new attack technique. If anything, everything here is old — a flawed trust check, in-band signaling, blocklists losing, misunderstood credential scope, workflow composition. What's new is that the thread stitching these together is natural language. The moment an issue body becomes a command, everyone who can open an issue becomes a potential holder of CI execution rights.

Anthropic's response was fast and conscientious (a four-day fix, documented residual risk, a paid bounty). Cline's response was slow, and that delay led to an actual unauthorized release. What the two contrasts tell you is about the importance of process, not about which side "solved" injection. Nobody has solved it. The vendor's own documentation says so, the researcher proved it with 50 findings, and a real breach happened in the meantime.

So if you're attaching an agent to CI right now, the better move is to change the question. Not "how do I stop injection" but "assuming injection succeeds, what can leak from this runner." The second question has an answer. Nobody, as of July 2026, can answer the first.

References