미니 샤이-훌루드 웜(The Mini Shai-Hulud Worm): TanStack NPM 침해 사고의 해부학
JavaScript 생태계는 여러 차례 공급망 공격에 직면해 왔지만, 최근 발생한 여러 TanStack 패키지와 @mistralai/mistralai 패키지의 침해 사고는 상당한 수준의 에스컬레이션을 의미합니다. 이는 단순한 일회성 자격 증명 탈취가 아니었습니다. 이는 CI/CD 파이프라인을 하이재킹하고, 개발자 비밀 정보를 훔치며, 생태계 전반으로 확산되는 것을 목적으로 설계된 자가 증식형 악성코드인 "미니 샤이-훌루드" 웜의 등장이었습니다.
이 사건은 현대적인 패키지 관리 방식에 내재된 시스템적 취약점과 자동화된 빌드 파이프라인에 우리가 부여하는 위태로운 신뢰를 상기시키는 강력한 경고입니다.
공격 벡터: 캐시 포이즈닝 및 CI/CD 하이재킹
공격은 단순한 비밀번호 유출로 시작되지 않았습니다. 대신, GitHub Actions와 NPM 레지스트리를 포함하는 정교한 취약점 체인을 활용했습니다.
GitHub Actions 캐시 포이즈닝
이 웜은 캐시를 처리하는 방식과 관련된 GitHub Actions의 설계 문제를 악용했습니다. 포크(fork)된 저장소에 악성 커밋을 푸시함으로써, 공격자는 메인 저장소의 릴리스 워크플로우에서 사용되는 캐시를 오염(poisoning)시킬 수 있었습니다. 릴리스 프로세스가 브랜치 간에 공유되는 가변 캐시를 사용했기 때문에, 악성 코드가 합법적인 패키지의 빌드 프로세스에 주입되었습니다.
신뢰할 수 있는 게시(Trusted Publishing)의 역할
많은 현대적인 프로젝트는 장기적인 비밀 정보를 저장하지 않고 NPM에 게시하기 위해 "Trusted Publishing" (OIDC 토큰)을 사용합니다. 이는 평문 토큰에 비해 보안상 개선된 방식이지만, 새로운 사각지대를 만듭니다. 커뮤니티 구성원들이 언급했듯이, 만약 공격자가 CI 파이프라인 내에서 코드를 실행할 수 있다면, OIDC 토큰을 훔쳐서 악성 버전의 패키지를 레지스트리에 직접 게시할 수 있습니다.
"Trusted Publishing / 파이프라인 게시 방식은 로컬에서 작업할 때 npm publish를 차단하는 일반적인 2단계 인증(second factor)을 제거합니다... 저장소 관리자 토큰을 탈취하거나 파이프라인에 악성 코드를 주입할 수 있는 사람은 누구나 아주 쉽게 게시를 완료할 수 있습니다."
페이로드: 복수심에 불타는 웜
개발자나 CI 시스템에 의해 악성 패키지가 설치되면, 웜은 lifecycle scripts (특히 prepare 훅)를 통해 페이로드를 실행합니다.
"데드맨 스위치(Dead-Man's Switch)"
이 공격에서 가장 경악스러운 측면은 페이로드의 복수심이었습니다. 분석 결과, 악성코드는 Linux의 systemd user service 또는 macOS의 LaunchAgent로 "데드맨 스위치"를 설치했습니다. 이 스크립트는 탈취한 토큰을 사용하여 60초마다 GitHub API를 폴링(polling)했습니다. 만약 토큰이 취소되면—즉, 개발자가 침해 사고를 인지했다는 것을 의미합니다—스크립트는 rm -rf ~/를 실행하여 사용자의 홈 디렉토리를 삭제하려고 시도합니다.
자가 증식
이 웜은 스스로 퍼져나가도록 설계되었습니다. 설치된 환경에서 비밀 정보를 훔침으로써, 해당 사용자가 게시할 권한을 가진 다른 패키지들을 침해할 수 있게 되어, NPM 생태계 전반에 걸쳐 연쇄적인 효과를 일으킬 수 있습니다.
시스템적 실패와 "NPM 문제"
이 사건에 대한 커뮤니티의 반응은 좌절감이었으며, 이는 JS/TS 생태계에서 반복되는 여러 실패를 강조합니다:
- Lifecycle Scripts:
postinstall및prepare스크립트가 기본적으로 실행된다는 사실은 2026년의 관점에서 볼 때 많은 이들에게 "부정행위"로 간주됩니다. 이러한 스크립트들은 악성코드 실행의 손쉬운로운 실행 벡터를 제공합니다. - NPM의 Unpublish 정책: TanStack은 영향을 받은 패키지를 즉시 unpublish할 수 없었다고 보고했습니다. NPM의 정책상 의존성(dependents)이 존재하는 경우 unpublish가 불가능하기 때문입니다. 이로 인해 그들은 NPM 보안팀이 서버 측에서 tarball을 제거하도록 요청해야 했으며, 이 과정에서 악성코드가 여전히 사용 가능한 상태로 몇 시간 동안 지류되었습니다.
- Provenance Attestations: 패키지들이 SLSA provenance attestations를 포함하고 있었음에도 불구하고, 비판론자들은 악성코드가 침해된 CI 러너를 통해 직접 attestation을 생성할 수 있다면 이러한 인증은 무의미하다고 주장합니다.
환경을 보호하는 방법
유사한 공급망 공격의 리스크를 완섭기 위해, 개발자와 조직은 다음과 같은 기술적 보호 조치를 고려해야 합니다:
1. Lifecycle Scripts 비활성화
설치 과정에서 임의의 스크립트를 실행하는 것을 피하십시오. .npmrc 파일에서 다음과 같이 전역적으로 설정할 수 있습니다:
ignore-scripts=true
2. 의존성 쿨다운(Dependency Cooldowns) 도입
"제로 데이(zero-day)" 악성 릴리스를 피하는 가장 효과적인 방법 중 하나는 최소 릴리스 연령을 최소한으로 설정하는 것입니다. 이는 방금 게시된 패키키지를 자동으로 가져오지 않도록 하여, 커뮤니티가 악성코드를 탐지지하고 보고할 시간을 벌어줍니다.
다양한 매니저를 위한 설정 예시:
- npm:
.npmrc에min-release-age=7(일) - pnpm:
pnpm-workspace.yaml에minimumReleaseAge: 10080(분) - Bun:
bunfig.toml에minimumReleaseAge = 604800(seconds) - Yarn:
.yarnrc.yml에npmMinimalAgeGate: 7d
3. CI/CD 파이프라인 강화
- 릴리스 파이프라인 격리: 게시(publishing) 파이프라인은 메인 개발 프로젝트와 완전히 격리되어야 하며, PR 워크플로우와 캐시를 공유해서는 안 됩니다.
- 권한 제한:
pull_request_target작업은 캐시나 외부 사이드 이펙트를 유발할 수 있는 쓰기 권한을 절대 가져서는 안 됩니다. - 단계별 게시(Staged Publishing) 도입: CI 시스템이 아티팩트를 빌드하지만, 사람이 2FA를 사용하여 해당 아티팩트를 공개 레지스트리에 "승인(promote)"하는 모델로 전환해야 합니다.