npm 웜(Worm)이 31일 동안 공개되었습니다. 두 개의 파생형이 출시되었습니다.
요약
npm과 PyPI 패키지를 대상으로 한 Mini Shai-Hulud 웜의 공격 방식과 그 파생형인 Red Hat Miasma 사례를 분석합니다. 기존의 게시자 신뢰도 기반 방어 체계를 우회하여 CI/CD 파이프라인과 OIDC 토큰을 탈취하는 새로운 공격 패턴을 다룹니다.
핵심 포인트
- 기존의 게시자 신뢰도 및 행동 점수 산정 방식을 우회하는 CI/CD 파이프라인 하이재킹 발생
- 잘못 설정된 GitHub Actions 워크플로를 통해 OIDC 토큰을 탈취하여 악성 패키지 배포
- Red Hat 환경 침해를 통해 유효한 SLSA 빌드 출처를 가진 악성 패키지 재게시 사례 확인
- 타이포스쿼팅이 아닌 공급망 자체의 인증된 파이프라인을 공격하는 고도화된 방식
TeamPCP는 5월 12일에 Mini Shai-Hulud를 GitHub에 푸시했습니다. 한 달 이내에 두 개의 독립적인 캠페인이 이를 포크(fork)했습니다. 각각은 이전의 방어 체계가 살아남을 수 없었던 새로운 설치 시점 우회(install-time bypass) 방식을 고안해냈습니다. 이제 다음 패턴을 예측할 수 있을 만큼 양상이 명확해졌습니다.
2026년 5월 11일, TeamPCP는 단 한 번의 조직적인 파동을 통해 npm과 PyPI 전반에 걸쳐 172개의 패키지를 오염시켰습니다. TanStack, Mistral AI, OpenSearch, UiPath, Guardrails AI가 포함되었습니다. 메커니즘은 타이포스쿼팅(typo-squatting)이나 유출된 자격 증명이 아니었습니다. 그것은 TanStack 자체의 잘못 설정된 GitHub Actions 워크플로(workflow)였습니다. 악성코드는 주변의 OIDC 토큰을 npm 게시 권한과 교환하였고, 레지스트리에 정당하게 인증된 악성 타르볼(tarballs)을 배포했습니다.
24시간 후, TeamPCP는 소스 코드를 공개했습니다. Akamai는 해당 릴리스 날짜를 5월 12일 저녁으로 기록했습니다. 이 웜(worm)은 스타터 키트(starter kit)가 되었습니다.
원본은 점수 산정 방식으로는 잡을 수 없었습니다
이에 대해 솔직해지고 싶습니다. TeamPCP가 5월 11일에 타격한 패키지들은 신뢰도가 높았습니다. @tanstack/react-router는 주간 다운로드 수가 1,970만 회에 달하며, 5명의 게시자, MIT 라이선스, 활발한 개발이 이루어지고 있습니다. 이 패키지는 Commit의 행동 감사(behavioral audit)에서 91점을 기록했습니다. 공격은 게시자를 침해한 것이 아니었습니다. 공격은 해당 게시자들이 배포를 위해 사용하는 파이프라인(pipeline)을 침해했습니다.
게시자 집중도, 유지 관리자 연속성, 그리고 릴리스 주기(release cadence)를 측정하는 점수 산정 계층은 @tanstack/react-router를 플래그(flag)로 표시하지 않을 것입니다. 표시해서도 안 됩니다. 그러한 신호들은 제 역할을 수행하고 있는 것입니다. 그것들은 건강한 패키지를 설명합니다. TanStack 공격은 게시자 계층을 완전히 건너뜀으로써 그 신호들을 우회했습니다.
만약 우리가 방어해야 했던 유일한 캠페인이 상위 50개 npm 패키지를 대상으로 한 CI/CD 파이프라인 하이재킹(hijack)뿐이었다면, 행동 점수 산정(behavioral scoring)은 무의미했을 것입니다. 하지만 다음에 온 것은 그런 것이 아니었습니다.
파생형 #1: Red Hat Miasma (6월 1일)
소스 공개 20일 후, JFrog는 32개의 @redhat-cloud-services 패키지에 걸쳐 96개 버전이 악성 preinstall 훅 (hook)과 함께 재게시되었다고 보고했습니다. 해당 패키지들은 Red Hat의 자체 빌드 환경이 조용히 침해된 이후, 해당 환경에 의해 서명된 유효한 SLSA 빌드 레벨 3 (SLSA Build Level 3) 출처 (provenance)를 보유하고 있었습니다.
페이로드 (payload)은 공격자가 생성한 데이터 유출 (exfiltration) 저장소의 설명을 "Miasma: The Spreading Blight"로 설정했습니다. 이 문자열은 이제 오픈 소스 코드로부터 파생되는 모든 하위 단계의 필드 마커 (field marker) 역할을 합니다. 이것은 TeamPCP 마커가 아닙니다. 바이너리 (binary)를 실행한 사람의 것입니다.
Red Hat의 패키지 점수는 65–83점이었습니다. 이는 높은 점수입니다. 다시 한번, 공격은 평상시에는 건강한 패키지들에 침해된 인프라를 사용했습니다. 출처 (provenance)는 여기서 방어 계층 역할을 하기로 되어 있었습니다. 하지만 그것이 멀웨어 (malware)에도 서명해 버렸습니다.
파생형 #2: Phantom Gyp (6월 3일)
Red Hat 사건 발생 48시간 후, 라이프사이클 스크립트 (lifecycle-script) 방어 체계가 무력화되었습니다. 그래서 다음 파동은 라이프사이클 스크립트를 사용하지 않았습니다. 157바이트 크기의 binding.gyp 파일이 설치 중에 node-gyp rebuild를 트리거합니다. 이는 2012년부터 네이티브 애드온 (native addons)이 빌드되어 온 방식입니다. StepSecurity는 이 기술을 "Phantom Gyp"라고 명명했습니다.
하지만 여기서 아무도 강조하지 않은 부분이 있습니다. Phantom Gyp가 타격한 패키지들은 TanStack 파동과는 완전히 다릅니다. 이들은 전혀 높은 신뢰도를 가진 패키지들이 아닙니다.
| 파동 | 예시 패키지 | 점수 | 게시자 수 | 주간 다운로드 |
|---|---|---|---|---|
| TanStack (5월 11일) | @tanstack/react-router | 91 | 5 | 1,970만 |
| ... |
점수 프로필이 반전되었습니다. TeamPCP는 파이프라인을 통해 레지스트리 (registry)의 최상단을 노렸습니다. 반면 파생형들은 게시자 자체를 통해 롱테일 (long tail) 영역으로 파고들고 있습니다. 이들은 검토할 제2의 눈이 없는 단일 게시자 계정을 탈취한 다음, 해당 계정이 이미 소유하고 있는 무엇이든 재게시합니다.
패턴을 명확히 기술하자면
23일 동안 발생한 하나의 멀웨어 (malware) 패밀리의 세 차례 파동:
- TanStack (5월 11일): 높은 신뢰도의 패키지 (high-trust packages), CI/CD 파이프라인 (CI/CD pipeline)을 공격 표면 (attack surface)으로 활용,
postinstall을 실행 경로 (execution path)로 사용. - Red Hat (6월 1일): 중간 신뢰도의 패키지 (medium-trust packages), 빌드 환경 (build environment)을 공격 표면으로 활용,
preinstall및 유효한 출처 (valid provenance)를 실행 경로로 사용. - Phantom Gyp (6월 3일): 낮은 신뢰도의 패키지 (low-trust packages), 단일 발행자 계정 탈취 (single-publisher account takeover)를 공격 표면으로 활용,
binding.gyp를 실행 경로로 사용 — 라이프사이클 훅 (lifecycle hook)을 전혀 사용하지 않음.
각 파생형은 이전 공격을 차단했던 방어 체계를 우회합니다. 이들은 기술을 재사용하지 않습니다. 그리고 반복될 때마다 타겟 프로필은 신뢰도 구배 (trust gradient)를 따라 낮아집니다: 91에서 65–83으로, 다시 28–49로 이동합니다.
이것이 다음 파동에 시사하는 바
TanStack 스타일의 공격에는 CI/CD 태세 (posture) 방어가 필요합니다: Actions를 태그가 아닌 커밋 SHA (commit SHA)로 고정하고, OIDC 범위 (OIDC scopes)를 잠그며, 러너 (runner) 상에서 메모리 스크래핑 (memory-scraping) 동작을 하는 Actions를 스캔해야 합니다. 행동 기반 점수 산정 (behavioral scoring)은 이러한 공격을 잡아내기에 적절한 계층이 아니며, 저는 그렇지 않은 척하지 않겠습니다.
Phantom Gyp 측면은 바로 행동 기반 점수 산정이 존재해야 하는 전형적인 형태입니다. 단일 발행자. 정기적인 릴리스 주기 없음. 제한된 채택률. 커뮤니티의 감시 부재. 기술은 변합니다 (오늘은 binding.gyp; 다음 달에는 아직 아무도 다루지 않은 무언가). 하지만 침해된 계정의 구조적 프로필은 변하지 않습니다. 그것이 바로 점수 산정 계층이 구축된 상수입니다.
다음 파생형에 대한 합리적인 추측: 또 다른 라이프사이클 미사용 실행 경로 (npm-shrinkwrap.json 조작, files 필드 스머글링 (smuggling), 리졸버 (resolver) 내의 무언가)를 사용하며, 점수 프로필이 30대인 또 다른 계층의 단일 발행자 패키지를 타겟팅할 것입니다. 만약 제 예측이 틀리더라도, 그 대가는 저렴할 것입니다. 어떤 경우든 방어 방법은 동일하기 때문입니다.
지금 당장 해야 할 일
CI를 운영 중이라면: 모든 GitHub Action을 태그가 아닌 커밋 SHA로 고정하세요. OIDC 토큰의 범위를 최소한의 게시 표면 (publishing surface)으로 제한하세요. TanStack 패턴은 CI에서의 의존성 설치 (dependency installs)를 통해 침투합니다. 여러분의 러너를 레지스트리 자격 증명 (registry credentials)과 동일한 폭발 반경 (blast radius)을 가진 것으로 취급하십시오.
의존성 (dependencies)을 선택하고 있다면: 파생형 공격의 파도를 맞고 있는 롱테일 (long-tail) 패키지들은 모두 60점 미만의 점수를 기록하고 있습니다. 락파일 (lockfile)을 행동 분석 감사 (behavioral audit)를 통해 실행해 보면, 다음 변종이 배포되기 전에 awaitly, autotel, node-env-resolver와 동일한 위험 프로필 (risk profile)을 가진 패키지가 무엇인지 확인할 수 있습니다.
npx proof-of-commitment
이 명령은 TanStack 피해자 (높음)와 Phantom Gyp 피해자 (낮음)를 구분해냈던 것과 동일한 신호들을 사용하여 프로젝트의 모든 의존성을 점수화합니다. 툴킷은 공개되어 있습니다. 패턴도 공개되어 있습니다. 롱테일 영역에 대한 방어책은 단 하나의 명령어로 가능합니다.
원문은 getcommit.dev에 게시되었습니다. Commit은 npm, PyPI, Cargo, Go 패키지를 행동적 약속 (behavioral commitment) 기준으로 점수화하며, 이는 별점 (stars), README, 또는 다운로드 수보다 조작하기 어려운 신호입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기