NOW Briefing
Snowflake .NET 커넥터 GitHub Actions 명령 주입 취약점
■ 개요
GitHub Actions(저장소 이벤트에 반응해 스크립트를 자동 실행하는 GitHub의 CI/CD 기능)는 이슈 등록·PR 생성 같은 이벤트를 트리거로 삼습니다. 문제가 된 워크플로우는 Snowflake의 공개 저장소 snowflakedb/snowflake-connector-net에서 새 이슈가 열리면 Jira 티켓을 자동 생성했습니다. 이슈 등록은 인증된 계정이면 누구나 할 수 있고 저장소가 공개돼 있어서, 이 워크플로우의 실행 트리거는 인터넷 전체에 열려 있었습니다.
보안 기업 Wiz의 분석에 따르면, 연구팀은 이슈 제목에 셸 구문을 삽입해 러너(워크플로우를 실제로 실행하는 가상 머신)에서 임의 명령을 실행했습니다. 이어 워크플로우에 주입돼 있던 시크릿(저장소에 암호화 저장된 자격증명) 가운데 Jira API 토큰을 외부 서버로 반출했습니다.
해당 토큰은 qa@snowflake.net 계정의 것으로, Snowflake의 엔지니어링·보안 컴플라이언스·버그바운티 추적 프로젝트를 읽을 수 있었습니다. Wiz는 2026년 6월 23일 취약점 신고 플랫폼 HackerOne으로 이를 제보했습니다. Snowflake는 "즉시 조사·조치했으며 무단 접근 정황은 발견되지 않았다"고 밝혔으며, 토큰은 2026년 6월 24일 교체됐습니다. 문제는 저장소의 CI 워크플로우에 한정돼 .NET 커넥터 배포 패키지 자체에는 영향이 없었으며, CVE도 등록되지 않았습니다.
취약한 워크플로우가 기본 브랜치에 들어간 PR에는 Copilot Autofix(코드 스캐닝이 찾은 취약점의 수정안을 AI가 제안하는 GitHub 기능)가 공동 작성자로 기록돼 있습니다. 다만 Wiz는 2026년 8월 17일 공개 이후 주석으로 "Copilot이 병합된 코드 변경을 확인한 공동 작성자로 기록됐을 뿐, 해당 코드 변경이 AI 보조로 이뤄졌는지는 불확실하다"고 정정했습니다. 사람이 취약점을 넣고 자동 수정이 이를 잡아내지 못했을 가능성도 남아 있습니다. AI 관여 여부와 별개로, 이미 안전하게 작성돼 있던 입력 처리 방식이 리뷰를 통과해 위험한 방식으로 되돌아갔다는 점이 이번 사례의 핵심입니다.
■ 요약
| 항목 | 내용 |
|---|---|
| 식별자 | • CVE 미할당 • 연구자(Wiz) 공개 및 벤더 자체 수정으로만 처리됨 |
| 심각도 | • 공식 CVSS 없음 • 저장소 권한 없이 트리거 가능하며 임의 명령 실행과 Jira API 토큰 반출이 확인됨 |
| 취약점 유형 | GitHub Actions 워크플로우 스크립트 인젝션 (신뢰 불가 입력의 셸 전개) |
| 영향/위험 | • 저장소 권한이 없는 외부 사용자가 러너에서 임의 명령 실행 가능 • 해당 작업에 전달된 Jira API 토큰 반출 확인 • 토큰 권한 범위의 내부 Jira 프로젝트 열람 가능 |
| 취약 버전 | • 대상: snowflakedb/snowflake-connector-net의 .github/workflows/jira_issue.yml• 노출 구간: 2026-06-18 병합 ~ 2026-06-23 수정 전 기본 브랜치 • .NET 커넥터 배포 패키지: 영향 없음 |
| 벤더 대응 현황 | • 2026-06-23 PR #1402 병합으로 수정 완료 • 2026-06-24 Jira 토큰 교체 • Snowflake는 무단 접근 증거를 확인하지 못했다고 발표 |
■ 이슈 제목 주입 경로
워크플로우는 이슈 제목과 본문을 셸 실행 블록 안에서 작은따옴표로 감싼 문자열로 전개한 뒤, sed로 따옴표를 이스케이프하는 방식을 썼습니다. 이 방식은 GitHub 표현식이 셸 실행 이전에 문자열로 치환된다는 점을 고려하지 못합니다. 이스케이프 처리가 도는 시점에는 이미 공격자 입력이 스크립트 본문의 일부가 돼 있고, 작은따옴표를 포함한 제목은 문자열 컨텍스트를 닫고 나와 뒤이은 내용을 명령으로 실행시킵니다.
입력 처리와 함께 실행 게이트도 작동하지 않았습니다. Wiz 분석에 따르면 조건문은 github.event.pull_request.user.login을 비교했는데, 이슈 이벤트에서는 github.event.pull_request가 항상 null입니다. 조건은 null != 'whitesource-for-github-com[bot]'으로 축약돼 언제나 참이 되고, 모든 GitHub 사용자가 게이트를 통과했습니다. 존재하지 않는 속성을 참조하는 조건문은 오류 없이 빈 문자열로 평가되기 때문에 눈에 띄지 않습니다.
# [패치 전]
run: |
# Escape special characters in title and body
TITLE=$(echo '${{ github.event.issue.title }}' | sed 's/"/\\"/g' | sed "s/'/\\\'/g")
BODY=$(echo '${{ github.event.issue.body }}' | sed 's/"/\\"/g' | sed "s/'/\\\'/g")
# ... 생략 ...
"summary": "'"$TITLE"'",
# [패치 후]
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_URL: ${{ github.event.issue.html_url }}
run: |
PAYLOAD=$(jq -n \
--arg title "$ISSUE_TITLE" \
--arg body "$ISSUE_BODY" \
# ... 생략 ...
"summary": $title,
수정 커밋은 이슈 제목·본문·URL을 env: 항목으로 옮겨 환경 변수로 전달하고, JSON 본문을 jq(JSON을 안전하게 만들고 다루는 명령줄 도구)의 --arg 인자로 조립하도록 바꿨습니다. 환경 변수 값은 스크립트 텍스트에 치환되지 않고 실행 시점에 데이터로만 읽히므로, 제목에 어떤 따옴표나 셸 메타문자가 들어와도 명령 경계를 넘지 못합니다. 이는 GitHub 공식 보안 강화 문서가 신뢰 불가 입력 처리에 권장하는 방식과 같습니다.
■ PoC
Wiz는 자사 자동화 공격 에이전트로 Snowflake의 GitHub 조직을 스캔해 이 워크플로우를 신뢰 불가 입력 전개 패턴으로 식별했습니다. 이어 이슈 제목에 문자열 컨텍스트를 닫는 셸 구문을 넣어 러너에서 명령이 실행되는 것을 확인했고, 워크플로우에 주입된 Jira 자격증명을 외부 수집 서버로 전송하는 방식으로 토큰 반출까지 성공했습니다.
공개 PoC 기준으로 이 공격에는 저장소 권한도 리뷰 승인도 필요하지 않았고, GitHub 계정으로 공개 이슈를 여는 것만으로 충분했습니다. 원문 블로그는 사용된 페이로드의 일부를 공개하고 있습니다. 이 글에서는 그대로 재사용 가능한 형태가 되지 않도록 코드 인용을 생략했으며, 전체 내용은 참고 자료의 원본 링크에서 확인할 수 있습니다.
⚠️ WARN
동일한 패턴의 워크플로우는 공개 저장소라면 외부 스캐너에 그대로 노출됩니다.
실제로 이 사례에서도 자동화 스캔이 저장소 목록에서 취약 워크플로우를 먼저 찾아냈습니다.
노출 기간이 5일에 불과했더라도 해당 워크플로우가 참조한 시크릿은 유출된 것으로 간주하고 처리해야 합니다.
■ 탐지 및 점검
• grep -rn '\${{ *github\.event' .github/workflows/로 워크플로우를 검색해, run: 블록 안에서 이벤트 페이로드가 직접 전개되는 점검 후보를 우선 찾습니다.
• issues, issue_comment, pull_request_target, discussion 트리거를 쓰면서 시크릿을 참조하는 워크플로우를 목록으로 만들고, 각각의 입력 처리 방식을 개별 검토합니다.
• 워크플로우 if: 조건이 해당 이벤트에 존재하지 않는 속성(이슈 이벤트에서의 github.event.pull_request.* 등)을 참조하는지 확인합니다. 존재하지 않는 속성은 빈 문자열로 평가돼 조건이 항상 참이 됩니다.
• Actions 실행 로그에서 이슈 제목·본문을 다루는 잡이 워크플로우 정의에 없는 외부 도메인으로 요청을 보낸 기록이 있는지 점검합니다. oast·interactsh 계열 콜백 도메인이나 예상 밖의 curl·base64 호출이 대표적인 신호입니다. 다만 이런 신호 하나만으로 침해를 확정하지 말고, 해당 잡의 실행 시각·트리거한 이슈 번호·시크릿 참조 여부를 함께 대조해 추가 점검 대상으로 다룹니다.
• GitHub 코드 스캐닝을 워크플로우 파일에도 적용해, 신뢰 불가 입력 전개 패턴이 병합 전에 경고로 잡히는지 확인합니다.
✅ CHECK
점검 범위는 공개 저장소에 한정하지 않습니다.
비공개 저장소라도 조직 구성원이나 외부 협력사 계정이 이슈를 열 수 있다면 동일한 트리거 조건이 성립합니다.
■ 대응 방안
• 이슈·PR 제목이나 본문을 셸에서 사용할 때는 env: 블록으로 환경 변수에 담아 전달하고, JSON 조립이 필요하면 jq --arg처럼 값을 데이터로 처리하는 도구를 사용합니다. 이 사례처럼 신뢰할 수 없는 입력을 스크립트에 먼저 삽입한 뒤 문자 치환하는 방식으로는 인젝션을 막을 수 없습니다.
• 취약한 워크플로우가 참조했던 시크릿은 노출 여부와 관계없이 즉시 폐기하고 재발급합니다. 이번 사례처럼 로그에 흔적이 없어도 반출은 가능합니다.
• 자동화용 계정의 권한을 실제 필요한 범위로 축소합니다. 티켓 생성만 필요한 토큰이 여러 프로젝트를 읽을 수 있었던 점이 이번 건의 피해 범위를 키웠습니다.
• .github/workflows/ 경로를 코드 소유자 지정 파일(CODEOWNERS)로 보호해 워크플로우 변경에 보안 담당자의 리뷰를 강제합니다. AI 보조 제안이 포함된 PR도 예외로 두지 않습니다.
• 자동 수정 제안을 검토할 때는 제안된 코드 자체의 문법뿐 아니라, 기존에 적용돼 있던 안전한 처리 패턴이 제거되지 않았는지 함께 확인합니다.
💡 TIP
워크플로우 변경 리뷰는 diff의 추가된 줄만 보기 쉽습니다.
삭제된 줄에서 환경 변수 경유나 파라미터 바인딩 같은 방어 장치가 사라졌는지 반드시 함께 확인해야 합니다.
[참고 자료]
• Wiz 리서치 - Snowflake 저장소 CI/CD 스크립트 인젝션 분석
• The Hacker News - Snowflake GitHub Actions 결함 보도
• The Register - 결함 원인 귀속에 대한 후속 정정 보도
• Snowflake .NET 커넥터 - 워크플로우 수정 PR #1402



