- 공개된 GITHUB_TOKEN이 짧게 유효한 틈을 타면, CodeQL을 쓰는 여러 저장소의 GitHub Actions 워크플로에서 코드 실행으로 이어질 수 있는 공급망 공격 경로가 열릴 수 있었음
- 토큰은
github/codeql-action저장소의 실패한 디버그 워크플로가 올린 workflow artifact에 들어 있었고,contents: write,actions: write,packages: write권한을 가진 GitHub App 설치 토큰이었음 - 공격 성패는 artifact 업로드 후 작업 종료 전까지 토큰을 내려받아 쓰는 경쟁 조건에 달렸으며, 실제 로그 기준 약 1.022초, 관찰상 약 2초 안에 branch 생성,
poc.txtpush, tag 생성이 성공함 - 기본 CodeQL 설정이 내부적으로
github/codeql-action의 v3 tag를 실행하고, 해당 tag가 불변이 아니었다는 점 때문에 공격자가 tag를 악성 commit으로 옮기면 기본 CodeQL 워크플로 사용 저장소까지 영향을 받을 수 있었음 - GitHub는 보고 약 3시간 뒤 디버그 artifact 업로드를 비활성화하는 PR을 냈고 CVE-2025-24362를 배정했으며, advisory에서 플랫폼이나 시스템 침해 증거는 없다고 밝힘
공개된 artifact에서 시작된 CodeQL 공급망 공격 경로
- 공개된 secret 하나가 GitHub CodeQL에 대한 잠재적 공급망 공격으로 이어질 수 있었음
- 해당 secret은 한 번에 약 1.022초 동안만 유효했지만, 그 안에 공격자가 GitHub Actions 워크플로 내부에서 코드를 실행할 수 있는 단계들을 수행할 수 있었음
- 영향 범위는 GitHub Cloud와 GitHub Enterprise 모두에 닿을 수 있었음
- 가능한 피해는 다음과 같았음
- CodeQL을 사용하는 private 저장소의 소스 코드 유출
- CodeQL 워크플로 job 안의 GitHub Actions secrets 탈취
- CodeQL 워크플로가 실행되는 내부 인프라에서 코드 실행
- CodeQL을 쓰는 저장소에서 GitHub Actions Cache를 사용하는 워크플로의 secrets 탈취
- GitHub advisory 기준으로 GitHub는 플랫폼이나 시스템 침해 증거를 발견하지 못함
secret을 찾아낸 방식
- GitHub Actions workflow artifact를 내려받고 압축을 재귀적으로 푼 뒤, Nosey Parker로 secret을 스캔하는 Actions Artifacts Secret Scanner가 사용됨
- 이 기능은 Chariot에 통합됐고, Gato 기능으로 오픈소스화됨
- 하루 동안 스캔한 뒤
github/codeql-action저장소 artifact에서 토큰이 발견됨- 대상 artifact는 “PR Check – Debug artifacts after failure” 워크플로가 업로드한
my-debug-artifactszip이었음 - 내부의
my-db-java-partial.zip을 풀자 crash report 안에서ghs_로 시작하는 GitHub Token이 탐지됨 - 수동 확인 결과 GitHub Runner의 환경 변수가 담긴 파일 안에 저장된 GitHub App 설치 토큰이었음
- 대상 artifact는 “PR Check – Debug artifacts after failure” 워크플로가 업로드한
GitHub Actions, artifact, CodeQL의 연결 구조
- GitHub Actions는 YAML workflow에 정의된 작업을 runner에서 실행하는 CI/CD 플랫폼임
- 모든 workflow run은 자동 생성되는 GitHub App 설치 토큰인 GITHUB_TOKEN을 생성함
- runner는 이 토큰으로 GitHub에 인증해 workflow가 요구하는 작업을 수행함
- 권한은 workflow 파일, 저장소, 조직 수준에서 설정 가능함
- GitHub Actions workflow artifact는 workflow가 나중에 쓰기 위해 GitHub Actions에 저장하는 파일임
- 기본적으로 저장소 읽기 권한이 있는 사람에게 접근 가능함
- 최대 90일 동안 저장됨
- CodeQL은 GitHub의 코드 분석 엔진이며, GitHub 저장소에서 정적 분석을 수행해 취약점을 찾는 데 쓰임
- CodeQL 같은 보안 도구는 민감한 시스템과 데이터에 접근해야 하는 경우가 있어, 손상되면 백도어가 될 수 있음
토큰 권한과 경쟁 조건
- 워크플로 로그의 “Setup Job” 단계에서 해당 GITHUB_TOKEN이 전체 쓰기 권한을 가진 것으로 확인됨
- 특히 중요한 권한은 다음과 같았음
contents: write: branch 생성, tag 생성, release artifact 업로드 가능actions: write: Actions 작업과workflow_dispatchevent 트리거 가능packages: write: package 업로드 가능
- 토큰은 해당 workflow job이 실행되는 동안만 유효했기 때문에 공격에는 세 조건이 필요했음
- 토큰에 write 권한이 있어야 함
- job 완료 전 artifact를 가져올 수 있는 upload artifact API v4를 사용해야 함
- artifact 업로드와 job 완료 사이 시간이 토큰 다운로드, 압축 해제, 사용에 충분해야 함
- CodeQL은
actions/upload-artifactaction을 직접 쓰지 않고 소스 코드에서 upload artifact client를 수동 사용했으며, 코드 주석상 version 4를 사용하고 있었음 - raw GitHub 로그 기준 시간 간격은 매우 짧았음
- “Finalizing artifact upload”: 17:22:09.888
- “Cleaning up orphan processes”: 17:22:10.911
- 계산상 약 1.022초였고, 관찰상 cleanup 이후 약 1초 더 토큰이 유효해 약 2초로 간주됨
PoC: artifact_racer.py 실행
- 검증용 Python 스크립트
artifact_racer.py가 작성됨 - 동작 흐름은 다음과 같았음
github/codeql-action저장소에서 “PR Check – Debug artifacts after failure” 워크플로가 시작되는지 계속 질의함- 실행 중인 workflow의 artifact를 모니터링함
- artifact가 보이면 다운로드하고 GITHUB_TOKEN을 추출함
- 추출한 토큰으로 새 branch를 만듦
- 해당 branch에
poc.txt라는 빈 파일을 push함 - 해당 commit에 새 tag를 생성함
- artifact 크기가 약 21MB였기 때문에 시간 내 성공 가능성이 있다고 판단됨
- 테스트 저장소에서 성공한 뒤
github/codeql-action저장소에 대해 실행됨 - 약 2시간 뒤 워크플로가 실행됐고, 스크립트는 토큰을 가져와 branch 생성, file push, tag 생성을 성공함
tag 조작이 키운 영향 범위
- 공격자는 해당 토큰으로 보호되지 않은 branch에 악성 코드를 추가할 수 있었음
- 더 은밀하게는 merge 전 feature branch에 작은 악성 변경을 넣고 merge를 기다리는 전략도 가능했음
- tag 생성 권한도 중요했음
- 악성 코드가 있는 commit에
v3tag를 붙이면,codeql-action@v3를 수동 사용하는 사용자가 악성 코드를 실행할 수 있음
- 악성 코드가 있는 commit에
- 더 큰 영향은 기본 CodeQL 설정에서 나왔음
- 저장소 설정에서 CodeQL을 기본 설정으로 활성화하면, 저장소 workflow 목록에는 보이지 않는 특수 GitHub Actions workflow가 실행됨
- 이 workflow는 저장소 checkout, CodeQL 초기화, CodeQL scan, 결과 업로드를 수행함
- 내부적으로
github/codeql-action저장소의 action을 v3 tag가 가리키는 commit으로 실행함
- 당시
v3tag는 불변이 아니었고, GitHub가 권장하는 workflow pinning도 사용되지 않았음 - 공격자가 손상된 GITHUB_TOKEN으로
v3tag를 악성 commit으로 옮기면, 기본 CodeQL workflow를 사용하는 저장소들이 악성 CodeQL action을 실행할 수 있었음 - CodeQL action은 실행 대상 저장소의 소스 코드를 checkout하므로, 악성 action은 CodeQL 기본 설정을 쓰는 저장소의 소스 코드를 유출할 수 있었음
GitHub Actions Cache poisoning 경로
- 기본 CodeQL action의 GITHUB_TOKEN은 read 권한만 있으므로, 기본 설정만으로는 repository write 작업이나 release 백도어,
workflow_dispatch를 통한 secrets 탈취를 바로 수행할 수 없었음 - 대신 기본 CodeQL action은 저장소의 main branch에서 실행됨
- GitHub 저장소의 main branch는 저장소 전체에서 사용될 cache entry를 쓸 수 있어 GitHub Actions cache poisoning 기회가 생김
- GitHub Actions Cache Poisoning은 Adnan Khan의 글에서 다뤄진 기법이며, Cacheract는 cache poisoning을 통해 build pipeline에 지속성을 남기는 malware임
- 공격자가 CodeQL workflow에서 Cacheract를 배포하면 다음 흐름이 가능했음
- cache entry 예측
- 해당 entry를 악성 action으로 덮어쓰기
actions/cache를 쓰는 workflow에서 코드 실행 획득- 해당 workflow의 GitHub Actions secrets와 권한 높은 GITHUB_TOKEN 탈취
- 악성 CodeQL action이 발견되어 취약점이 조치되더라도, Cacheract는 cache poisoning을 계속 수행할 수 있었음
- CodeQL과
actions/cache를 함께 쓰는 주요 repo 예시로 Homebrew, Angular, Grafana가 확인됨
CVE-2025-24362와 수정
- 이 공개의 결과로 CVE-2025-24362가 배정됨
- 공개된 GITHUB_TOKEN은 실패한 code scanning workflow 뒤 CodeQL Action이 업로드한 debug artifact 안에 있었음
- CodeQL Actions 저장소는 의도적으로 실패를 유발하고 있었지만, 다른 CodeQL Actions 사용자도 비슷한 실패가 발생했다면 workflow 환경 변수로 자신의 secrets를 노출했을 수 있었음
- 이 문제는 CodeQL Action 3.28.3에서 수정됨
- 가장 큰 잠재 영향은 CVE 자체보다 CodeQL Actions 저장소에 대해 취약점을 악용하고 CodeQL 사용자에게 공급망 공격을 수행하는 경로에 있었음
GitHub 대응과 권장 완화책
- GitHub의 대응 일정은 다음과 같았음
- 2025년 1월 22일 15:13 UTC: GitHub에 보고 제출
- 2025년 1월 22일 17:48 UTC: GitHub가 접수 확인
- 2025년 1월 22일 18:28 UTC: GitHub가 취약점을 확인하고 “PR Check – Debug artifacts after failure” workflow를 임시 비활성화했으며, debug artifact 업로드를 비활성화하는 PR을 제출함
- 2025년 1월 24일: GitHub가 CVE-2025-24362를 배정하고 security advisory를 공개함
- GitHub Actions workflow artifact의 secret 노출 위험을 줄이기 위한 조치는 다음과 같음
- workflow artifact로 특정 파일이나 디렉터리만 업로드함
- 환경 변수,
.git/config, runner의<path_to_runner_dir>/_work/_temp/디렉터리 파일을 포함한 artifact 업로드를 피함 - GITHUB_TOKEN 권한을 read-only로 제한함
- artifact 업로드 전에 secret scan을 수행함