1P by GN⁺ | ★ favorite | 댓글 1개
  • 공개된 GITHUB_TOKEN이 짧게 유효한 틈을 타면, CodeQL을 쓰는 여러 저장소의 GitHub Actions 워크플로에서 코드 실행으로 이어질 수 있는 공급망 공격 경로가 열릴 수 있었음
  • 토큰은 github/codeql-action 저장소의 실패한 디버그 워크플로가 올린 workflow artifact에 들어 있었고, contents: write, actions: write, packages: write 권한을 가진 GitHub App 설치 토큰이었음
  • 공격 성패는 artifact 업로드 후 작업 종료 전까지 토큰을 내려받아 쓰는 경쟁 조건에 달렸으며, 실제 로그 기준 약 1.022초, 관찰상 약 2초 안에 branch 생성, poc.txt push, tag 생성이 성공함
  • 기본 CodeQL 설정이 내부적으로 github/codeql-actionv3 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-artifacts zip이었음
    • 내부의 my-db-java-partial.zip을 풀자 crash report 안에서 ghs_로 시작하는 GitHub Token이 탐지됨
    • 수동 확인 결과 GitHub Runner의 환경 변수가 담긴 파일 안에 저장된 GitHub App 설치 토큰이었음

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_dispatch event 트리거 가능
    • packages: write: package 업로드 가능
  • 토큰은 해당 workflow job이 실행되는 동안만 유효했기 때문에 공격에는 세 조건이 필요했음
    • 토큰에 write 권한이 있어야 함
    • job 완료 전 artifact를 가져올 수 있는 upload artifact API v4를 사용해야 함
    • artifact 업로드와 job 완료 사이 시간이 토큰 다운로드, 압축 해제, 사용에 충분해야 함
  • CodeQL은 actions/upload-artifact action을 직접 쓰지 않고 소스 코드에서 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에 v3 tag를 붙이면, codeql-action@v3를 수동 사용하는 사용자가 악성 코드를 실행할 수 있음
  • 더 큰 영향은 기본 CodeQL 설정에서 나왔음
    • 저장소 설정에서 CodeQL을 기본 설정으로 활성화하면, 저장소 workflow 목록에는 보이지 않는 특수 GitHub Actions workflow가 실행됨
    • 이 workflow는 저장소 checkout, CodeQL 초기화, CodeQL scan, 결과 업로드를 수행함
    • 내부적으로 github/codeql-action 저장소의 action을 v3 tag가 가리키는 commit으로 실행함
  • 당시 v3 tag는 불변이 아니었고, GitHub가 권장하는 workflow pinning도 사용되지 않았음
  • 공격자가 손상된 GITHUB_TOKEN으로 v3 tag를 악성 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을 수행함

댓글과 토론

Hacker News 의견들
  • GitHub이 immutable actions를 빨리 밀어붙였다면 이런 영향도 훨씬 덜했을 것임
    이 기능이 지금 GitHub Actions 공격 범위의 70% 이상을 막아줄 거라고 계속 반복해서 말하게 됨. 매주 사고가 나는 걸 보면 이제는 출시할 때도 됐다고 봄
    [1] https://github.com/features/preview/immutable-actions

    • 아직 미리보기 상태라면 그럴 만한 이유가 있을 가능성이 큼. 심각한 버그, 보안 구멍, 서두르면 더 큰 피해를 낼 수 있는 호환성 깨짐 등이 있을 수 있음
  • 임시 토큰이 새 배포를 만들고 아티팩트 증명을 생성할 권한까지 왜 갖고 있었는지는 설명이 없음
    수정으로 디버그 로그를 껐다고 했지만, 코드 분석 엔진에 맞게 임시 토큰 권한을 더 적절하게 바꿨는지는 답하지 않았음

    • 다들 아는 오래된 이야기 같음. 만들던 엔지니어가 권한 거부를 만나자 모든 권한을 줬고, 나중에 최소 권한으로 줄이러 돌아오지 않은 것임
    • GitHub Actions 임시 토큰에 워크플로에서 정의한 권한 범위가 없으면, 저장소 설정에 따라 허용적인 기본 범위나 제한적인 기본 범위가 적용됨. 이 설정은 조직 수준에서도 모든 저장소를 제한하도록 구성할 수 있음
      과거에는 기본값이 허용적 설정뿐이어서, 오래된 조직과 저장소에는 안타깝게도 이 설정이 쓰이는 경우가 많음
      새 저장소를 만들면 상위 조직의 기본값을 상속하므로, 누가 바꾸지 않으면 이 안전하지 않은 기본값이 계속 남아 있음. 사용자 전체 설정은 없어서 개인 소유 새 저장소는 제한적인 기본값을 사용함. 새로 만든 조직도 더 나은 기본값을 쓰는 것으로 알고 있음
      [0]: https://docs.github.com/en/actions/security-for-github-actio...
    • 임시 액션 토큰은 기본적으로 전체 쓰기 권한을 가짐. 읽기 전용 버전을 쓰려면 명시적으로 선택해야 함

      Read and write permissions
      Workflows have read and write permissions in the repository for all scopes.
      문서의 이 줄(https://docs.github.com/en/actions/security-for-github-actio...)만 읽으면 다르게 생각할 수 있음
      If the default permissions for the GITHUB_TOKEN are restrictive, you may have to elevate the permissions to allow some actions and commands to run successfully.
      하지만 우리 GitHub 조직에서는 "Read and write permissions"가 기본값이었고, 그래서 저 문서 문장은 말이 안 된다고 확인할 수 있음

    • 2023년 Microsoft 해킹도 이와 비슷했음. CISA가 보안이 형편없다고 공개적으로 질타했던 사건인데, Microsoft가 사건을 설명하려고 올린 블로그 글에도 답하지 않은 질문이 너무 많았음
    • 디버그 로그를 끈 건 빠른 임시 조치였기를 바람. 최종 수정은 아니어야 함
  • CI와 CD는 완전히 분리된 환경이어야 한다는 확신이 점점 커짐. CI가 침해됐다고 해서 CD 관련 토큰이 유출되면 안 됨

    • 이 분야는 개인적으로 매우 관심이 큰데, 해법은 CD를 별도의 작은 특수 시스템으로 떼어내는 것보다는 CD를 다중 요소처럼 만드는 쪽이라고 봄
      예를 들어 "sub":"repo:octo-org/octo-repo:environment:prod"[1] 같은 조건을 반드시 만족해야 하게 하고, 시스템을 더 단단하게 만들고 싶으면 다른 [fun claims][]도 추가하면 됨
      1: https://docs.github.com/en/actions/security-for-github-actio...
      fun claims: https://github.com/github/actions-oidc-debugger#readme
    • 본질적으로 이것이 직무와 관심사의 분리가 보이는 방식임. 좋은 프로젝트 사례들도 이렇게 동작하는 경우가 있음. 구체적인 기법, 도구, CI와 CD의 경계는 최종 산출물의 성격에 따라 달라지지만, 개념적으로는 완전히 맞음
  • 대응 시간이 농담이 아니었음. GitHub의 대응은 꽤 인상적임

    • 전체 쓰기 권한이 있는 공개 토큰이 노출된 건 별로 인상적이지 않음
  • Prater라는 성을 가진 입장에서, 이 이름이 Praetorian에서 유래했다 보니 praetorian.com을 갖고 있었으면 정말 좋겠음

    • 그러려면 1995년 영화 “The Net”이 나오기 전에 그 생각을 했어야 했음
    • 그들의 gokart 프로젝트는 훌륭했음
  • 공개 GitHub Actions를 쓰는 건 문제를 자초하는 일이고, 워크플로 절차를 분석하지 않는다면 더 그렇음
    차라리 woodpecker나 그 밖의 좋은 CI 빌더들(circle, travis, gitlab 등)을 직접 호스팅하는 편이 나음

  • OpenZFS PR에 CodeQL을 적용해 두었음. 이건 OpenZFS에는 문제가 아님. 우리 코드는 비밀이 아니기 때문임 :)

    • 코드가 비밀이 아니어도 좋은 판단은 아니라고 봄. 공격자가 코드나 릴리스 아티팩트에 무엇이든 추가할 수 있었기 때문임
      그래도 적어도 빠르게 해결된 건 다행임
  • 이거 수정됐나?

    • 글과 여기 댓글에 나와 있듯이, 맞음. 1월에 신고된 뒤 GitHub가 3시간 안에 완화 조치했음
  • 이 사이트는 성능이 너무 나빠서 스크롤도 거의 못 하겠음