2P by GN⁺ 2일전 | ★ favorite | 댓글 1개
  • 오픈소스 패키지 @ctrl/tinycolor가 포함된 다수의 npm 패키지가 악성 버전에 감염되는 사건이 발생, 원인은 협업 저장소의 GitHub Actions 워크플로우를 통한 npm 토큰 탈취였음
  • 공격자는 broad 권한의 npm 토큰을 이용해 약 20개의 패키지에 악성 코드를 배포했으며, 이 중 @ctrl/tinycolor는 주간 다운로드 수가 200만 회에 달해 영향력이 컸음
  • 감염된 버전은 postinstall 단계에서 악성 페이로드를 실행했으며, GitHub 및 npm 보안팀이 빠르게 대응해 삭제 및 정리 조치가 이루어졌음
  • 작성자는 재발 방지를 위해 Trusted Publishing(OIDC) 전환, 토큰 권한 최소화, 2FA 의무화, pnpm 기능 활용 등 강화된 보안 계획을 마련
  • 이번 사건은 소프트웨어 공급망 보안의 취약성을 보여주며, npm 생태계 전반의 보안 기능 개선과 보안 관행 변화의 필요성을 드러낸 사례임

TL;DR

  • 악성 GitHub Actions 워크플로우가 공유 저장소에 푸시되어 npm 토큰을 탈취한 사건
  • 해당 토큰으로 공격자는 20개 패키지의 악성 버전을 배포했고, 그중 @ctrl/tinycolor는 다운로드량이 많아 파급력이 컸음
  • 개인 계정이나 레포지토리는 직접 침해되지 않았고, 피싱이나 로컬 악성코드 설치도 없었음
  • GitHub/npm 보안팀의 신속 대응으로 악성 버전이 제거되었고, 이후 깨끗한 버전을 다시 배포해 캐시를 정리했음

사건 발견 경위 (How I Found Out)

  • 9월 15일 오후, 커뮤니티 멤버 Wes Todd가 Bluesky DM을 통해 문제를 알려주었음
  • 이미 GitHub/npm 보안팀은 영향을 받은 패키지 목록을 정리하고 삭제 조치를 시작
  • 초기 단서로 'Shai-Hulud'라는 악성 브랜치명이 공유되었으며, 이는 Dune 세계관의 샌드웜 명칭에서 따온 것임

실제 발생한 일 (What Actually Happened)

  • 오래전 협업한 angulartics2 저장소에 여전히 admin 권한을 가진 협력자가 있었음
  • 해당 저장소에 보관된 npm 토큰이 악성 GitHub Actions 워크플로우에 의해 탈취됨
  • 공격자는 이 토큰으로 @ctrl/tinycolor 포함 약 20개 패키지를 배포했음
  • GitHub/npm 보안팀이 빠르게 악성 버전을 삭제했고, 작성자가 신뢰할 수 있는 새 버전을 다시 배포했음

영향 (Impact)

  • 악성 버전을 설치할 경우 postinstall 스크립트가 실행되어 보안 위협 발생
  • 영향받은 사용자들은 StepSecurity의 즉각적인 대응 지침을 참고할 것을 권장함

배포 환경 및 대응 계획 (Publishing Setup & Interim Plan)

  • 기존에는 semantic-release + GitHub Actions 조합으로 자동 배포를 진행
  • npm provenance 기능을 활용했지만, 유효한 토큰을 가진 공격자를 막을 수는 없었음
  • 향후에는 Trusted Publishing(OIDC) 도입으로 정적 토큰을 제거할 계획
  • 현재는 모든 토큰을 폐기하고 2FA 필수화, granular 권한의 토큰만 허용, pnpm의 minimumReleaseAge 기능 검토 등 추가적인 보안 조치를 적용 중

이상적 개선안 (Publishing Wishlist)

  • npm 계정 단위에서 OIDC 기반 Trusted Publishing 강제 옵션 제공 필요
  • provenance 누락 시 배포 차단 기능, semantic-release와 OIDC의 완전한 통합 지원 필요
  • GitHub UI에서 2FA 기반 수동 승인 배포 기능 제공 희망
  • Pro 구독 없이도 GitHub Environments 수준의 보호 기능을 활용할 수 있어야 함
  • npm 패키지 페이지에서 postinstall 스크립트 여부 표시 및 삭제된 버전의 사유 공개 필요
Hacker News 의견
  • 해당 저장소에는 여전히 GitHub Actions 시크릿, 즉 광범위한 퍼블리시 권한을 가진 npm 토큰이 남아있었음
    Trusted Publishing의 장점 중 하나는 더 이상 장기 유효한 퍼블리시 토큰을 쓸 필요가 없다는 점임
    이제는 CI VM에서 단기적으로 생성되는 토큰만 사용하며 토큰은 15분 동안만 유효함
    이미 PyPI, npm, Cargo, Homebrew 등 여러 생태계에서 적용 중임
    발행 과정이 실제로 좀 더 쉬워지기도 하므로 모두 사용해보는 것을 권장함
    문서가 여전히 불명확하게 느껴진다면 언제든 도움을 요청해도 됨
    생태계 관리자들은 이 기능이 확산되는 것을 적극적으로 바라는 분위기임
    Trusted Publishing 공식 문서 참고

    • npm에서 이제 Trusted Publishing이 가능해진 사실은 이번에 처음 알게 됨
      관련 소식
      이번 주말에 바로 셋업해볼 예정임

    • 이제는 이런 기능을 사용하는 프로젝트임을 저장소에 표시하는 플래그가 있으면 좋겠음
      그렇게 하면 이를 사용하지 않는 의존성 패키지를 쉽게 차단할 수 있음

  • 자동 배포 과정에 MFA(다단계 인증)를 포함시키는 논점이 충분히 주목받지 못한 것 같음
    CI 워크플로우로 퍼블리시를 하면서 MFA 프롬프트로 발행을 확정하는 건 문제가 없으나, 지난번에 살펴봤을 때는 코드 제공 목적으로 HTTPS 터널을 열어야 해서 복잡했음
    npm이나 GitHub에서 CI 중에 MFA 코드를 쉽게 제공·확정할 수 있는 방법을 바로 제공해줬으면 좋겠음

    • 패키지 퍼블리싱은 2단계가 있음: 패키지를 npmjs에 업로드하는 단계와, 실제로 사용자들에게 공개하는 단계임
      현재는 이 두 단계가 하나의 작업으로 묶여버림
      내 생각엔 이걸 분리해서, CI 시스템은 자동으로 빌드하고 업로드만 하게 하고
      업로드된 패키지를 진짜 배포하려면 사람이 npmjs 웹사이트에 직접 로그인하여 수동으로 퍼블리시 및 MFA를 거치게 하는 구조가 맞다고 봄

    • 사실 패키지 퍼블리시 자체가 필요 없는 개념 아닐까 하는 생각임
      VCS가 '진짜 소스'라면 별도 퍼블리싱 과정 없이 바로 사용하면 되지 않을까
      Go 언어는 실제로 이렇게 함
      패키지를 URL 기반으로 직접 import 하고 버전링도 태그로 관리함
      이렇게 하면 VCS만 신뢰하면 되기 때문에 추가적인 공격면이 줄어듦
      아카이브 파일을 따로 diff할 필요 없이 커밋 단위로만 확인하면 됨
      문제는 저장소를 이동하면 import 경로가 바뀐다는 점이지만, 이것도 일종의 장점이라고 볼 수 있음
      그 외에 별도의 퍼블리시 단계가 주는 이점이 뭔지 잘 모르겠음
      옛날 FTP로 tar 아카이브 업로드하던 시절의 유물 같음

  • 예전에 angulartics2라는 공유 저장소 작업을 했었음
    거기엔 아직도 광범위 퍼블리시 권한을 가진 npm 토큰이 담긴 GitHub Actions 시크릿이 있었음
    어느 협력자가 여러 프로젝트의 권한을 가지고 있기도 했고, 그게 여러 패키지가 동시에 영향을 받은 이유로 추정함
    Shai-Hulud라는 새로운 브랜치가 악성 github action workflow와 함께 force push됨
    관리자 권한 협력자라서 리뷰 필요 없이 바로 워크플로우가 실행됐고 npm 토큰이 유출됨
    유출된 토큰으로 20개 패키지에 악성 버전이 배포됨
    대부분 널리 쓰이지 않는 패키지지만 @ctrl/tinycolor는 한 주에 약 2백만 번 다운로드되는 인기 패키지임
    아직 이해가 안 되는 것은, angulartics2 저장소의 npm 토큰으로 어떻게 tinycolor까지 퍼블리시 할 수 있었냐는 점임

    • 나도 다른 사람의 npm 저장소에 관리자 권한이 있고 최근의 릴리스도 거의 내가 진행함
      관리자가 되고 나니까 그동안 묵혀 놨던 문제점들을 이참에 고치고 싶어서 커밋도 내 이름이 많아짐
      Github action으로 패키지를 퍼블리시하자는 쪽으로 거의 마음을 먹고 있었는데, 2FA로 직접 배포했을 때 실수로 마스터 아닌 상태를 배포할까봐 늘 걱정됐었음
      이런 문제 때문에 다른 관리자들과 논의하는 것도 미뤄왔는데, 결국 이렇게 된 걸 보니 미루길 잘한 느낌임
      정답이 뭔지는 모르겠지만, 자격 증명을 제3자에 맡기는 건 확실히 좋은 답은 아닌 것 같음

    • angulartics2의 npm 토큰이 왜 tinycolor 배포 권한을 가질까?
      이건 정말 전통적인 조직 해킹 루트 같음
      예전에 써 놓은 '과거의 죄'가 언젠가 발목을 잡음
      몇 년 전에도 우리 조직에 이런 일이 있었음
      세 번이나 교체한 옛날 에디터에 남은 보안 허점 때문에 서버에 업로드 허용이 가능해졌음
      빌드 타임에 다 찾아내는 게 아니고, 결국 URL 스캔으로 찾아냄

    • 설명이 부족했다면 미안함
      이 토큰은 내 npm 패키지 전체에 대한 글로벌 퍼블리시 권한을 가졌던 토큰임

  • 최근 10년 동안 수동 릴리스를 계속 주장해왔음
    늘 반발을 많이 받았지만, 요즘 그리 이상한 개념은 아닌 듯함
    CI/CD가 멋진 건 알지만 이번 사태나 최근 CF 이슈를 보면 오히려 자동화로 인해 심각한 문제가 더 잘 생길 수 있다는 증거가 많아짐
    한때 BigBank에서 일할 때는 프로덕션 배포에 최소 다섯 명이 함께 대기하고 많은 절차를 거쳤지만, 그래도 뭘 배포하는지 확실히 알 수 있었음

    • 완전 동감임
      GitHub Actions나 자동 릴리스 스크립트 때문이 아니라 예전처럼 직접 빌드해서 서명하고 tarball 올려서 검증하는 방식이 훨씬 안전하다고 생각함
      배포 시스템(예: Debian과 같은 배포 패키징 시스템)은 별도의 검증 단계를 거치기도 해서, xz 사태 때 인터넷 전체가 해킹되지 않은 이유이기도 함
      최소한 릴리스가 퍼블리시되기 전에 사람이 직접 바이너리에 서명하는 절차를 필수로 둬야 함
      공격자가 자신을 메인테이너로 추가하고 자기 키로 서명하는 문제도 있기 때문에, 배포 패키징 시스템처럼 신뢰된 키 관리가 병행되어야 더 안전함
      만약 내 위협 모델이 “GitHub 계정이나 API 키 하나라도 유출되면 사용자 전체가 뚫린다”에 근거한다면, 정말 합리적인지 스스로 되돌아봐야 함
  • 퍼블리싱에 2FA를 쓰는 것도 좋지만, 여러 명의 저자가 암호 서명으로 동의해야 훨씬 더 안전함
    한 명만 뚫린다고 공격이 성공하지는 않게 되어야 함

    • 많은 패키지는 저자가 단 한 명임

    • 여러 저자의 서명을 요구하는 것도 좋지만, 커밋, 태그, 산출물 등에 어떤 형태든 서명 검증이 이뤄진다면 대부분의 공격을 막을 수 있음
      배포 패키징은 서명 검증 지원이 철저하지만, 언어 패키지 매니저들은 그런 검증 체계가 부족함
      예시로 runc 공식 릴리스 과정은 모두 유지자 키로 서명되고, 키는 Yubikey 등에 보관
      배포판 시스템도 별도 키링을 관리하면서 공식 소스와 바이너리를 검증함
      만약 이런 과정이 있었다면 이번 공격도 여러 단계에서 차단됐을 것이라고 생각함
      CI에서 바로 빌드할 순 있어도, 최종적으로는 유지자가 직접 서명하는 구조가 필요
      언어 패키지 매니저에 이런 워크플로가 없다면 Trusted Publishing이 그나마 덜 나쁜 대안이긴 함
      하지만 GitHub 계정이 공격당하면(예: 쿠키 탈취) 바로 퍼블리시가 가능하긴 함
      GitHub에서는 Trusted Publishing에 타임아웃 등 보안설정을 지원하지만 공격자가 꺼버릴 수도 있음
      내 계정이 털려도 배포판에서는 내가 서명하지 않은 키로 이루어진 변경을 받아들이지 않으니, 상대적으로 안전함
      참고: 나는 SUSE 소속이지만, openSUSE, Arch, Gentoo와 같이 산출물 검증 지원이 더 늘어나길 바람
      관련 링크:

    • runc.keyring

    • keyring_validate.sh

    • release_sign.sh

    • openSUSE의 runc.keyring

  • 나는 토큰이 너무 싫음
    토큰은 그저 정적 비밀번호와 똑같음
    좀 더 제대로 된 인증 방식이 필요하다고 생각함
    예시로 Github을 AWS의 토큰 제공자로 쓰는 방식이 그나마 바람직하다고 봄
    Github-AWS OIDC 통합 방식
    이 방식은 예외적인 사례에 불과함

    • 머신 간 OIDC 플로우는 잘만 구현하면 안전할 수 있지만, 설정이 너무 복잡함
      그리고 결국 OIDC도 “더 복잡한 토큰”일 뿐인 느낌이 있음
      사람이 직접 확인하지 않는 자동화 환경이면 항상 어딘가에서 유출될 수 있는 뭔가가 남아있음(토큰이든 토큰 생성기든)
      이번 웜 사례에서도 OIDC는 근본 해결이 아니었음
      Github 워크플로가 뚫렸다면, OIDC든 아니든 환경에 임시 아이덴티티가 주입될 뿐임
      결국 미승인 사용자가 시크릿을 가진 워크플로를 실행할 수 없는 체계가 중요함
      세밀하게 권한을 나누려면 OIDC보다는 차라리 토큰 권한 범위를 줄이는 게 효과적일 수도 있음

    • 토큰의 원래 취지는 수명과 권한 범위(authZ)가 제한된다는 것임
      그런데 대부분의 경우 실제로 그렇지 않고 그냥 비밀번호처럼 정적으로 쓰임
      oauth, 상세 권한 제한이 가능한 biscuits 같은 대안이 존재하지만 실제로 잘 안 씀

    • Trusted Publishing은 이제 npm 등 여러 패키지 레지스트리에서 지원됨
      관련 소식

    • 다른 사람도 언급했지만, 토큰은 짧은 수명 혹은 수동 인증(MFA, 패스프레이즈 등) 뒤에서만 발급되어야 함

    • mTLS(TLS 클라이언트 인증서)를 쓰는 것이 정답에 가까운 방향임

  • 혹시 취약 npm 패키지 체크용 공개 도구/스크립트 아시는 분 계심?
    stepsecurity 페이지에는 그런 도구가 없는 듯 보임

    • 모든 걸 막을 수 있는 건 아니지만, provenance-action을 도입하는 것도 좋은 아이디어임
      provenance-action

    • 알려진 이슈에 대해선 npm audit을 쓰는 게 기본임

  • 로컬 2FA 기반 퍼블리싱은 지속 불가하다
    로컬 2FA 방식이 왜 지속 불가함?
    진짜 문제는 자동화 퍼블리싱 워크플로임
    대부분의 npm 패키지는 그렇게 자주 퍼블리시되거나 복잡한 릴리스 과정이 필요 없다 생각함
    사람이 직접 2FA 걸고 npm publish 하는 게 뭐가 그렇게 힘든 일임?
    이런 방식조차 귀찮으면 패키지 관리 개수를 재평가해야 함

    • 일리가 있음
      나는 로컬 퍼블리싱에서 잘못된 브랜치나 빌드누락 등 실수들이 좀 걱정됐다는 의미였음
  • 만약 CI 잡이 git 히스토리의 깊은 부분을 force push해서 변경한다면 어쩌나 생각됨

  • 현 체제(status quo)가 더는 제대로 작동하지 않는 상황임
    물론 OIDC 토큰, 제로 트러스트 솔루션 등 기술적 장점을 칭찬할 수는 있지만
    수백만 다운로드되는 npm 라이브러리 유지보수자들 중 상당수가 실제로는 해킹당하거나 npm이 아예 배포차단을 걸기 전까지 보안에 신경을 쓰지 않을 것임
    그리고 “의존성을 전부 없애고 표준 라이브러리만 남기자” 같은 실현 불가능한 주장도 나옴
    의존성 줄이는 건 좋지만, 이미 있는 문제엔 아무런 해결책이 못 됨
    현실적으로는 만 명, 십만 명이 npm을 떠나 모두 코드를 뜯어고치거나, 반대로 npm이 다운로드 많은 패키지 중심으로 2FA, OIDC 같은 규정을 강제하고, 지키지 않으면 아예 배포를 막는 방법밖에 없음
    어느 쪽이 현실적으로 더 실행 가능한지는 분명함
    그렇지 않으면 npm 평판은 바닥을 치게 되고 XKCD 927 상황이 올 뿐임