
npm을 끊을 수는 없습니다. 하지만 npm install, lockfile, CI 캐시, 릴리스 자동화를 더 이상 "배관"처럼 취급할 수도 없습니다. 이번 TanStack npm 공급망 공격은 특정 패키지를 썼느냐보다 더 불편한 질문을 던집니다. 악성 의존성이 우리 환경에서 실행됐다면, 프로덕션 전에 알아챘을까?
확인된 사건은 이렇습니다. 2026년 5월 11일 19:20~19:26 UTC 사이, @tanstack 네임스페이스의 42개 패키지에 84개 악성 버전이 게시됐습니다. 공격자는 npm 토큰을 훔친 것이 아니라 TanStack의 합법적인 GitHub Actions 릴리스 파이프라인을 장악해 OIDC 기반 trusted publishing으로 게시했습니다.1 OpenAI도 이 사건에 대한 별도 대응 글을 낼 만큼, 문제는 특정 프론트엔드 라이브러리 하나가 아니라 현대 개발 조직의 의존성 실행 경로 전체로 번졌습니다.2
"영향받았나"보다 먼저 물어야 할 것
사고 채널에서 흔히 나오는 첫 질문은 "우리 package.json에 TanStack 있나요?"입니다. 틀린 질문은 아니지만 충분하지 않습니다.
이번 악성 패키지는 설치 시점의 lifecycle script, GitHub dependency, router_init.js 같은 페이로드를 통해 실행될 수 있었습니다. 즉 위험은 "의존성 목록에 있었는가"가 아니라 "어디에서 실행됐는가"로 판단해야 합니다.
먼저 실행 지도를 그려야 합니다.
개발자 로컬 머신에서 설치됐는가
CI runner에서 설치됐는가
preview deployment나 storybook 빌드에서 실행됐는가
프로덕션 이미지 빌드에 포함됐는가
릴리스 권한이 있는 파이프라인에서 실행됐는가
같은 패키지를 설치했더라도, 빈 샌드박스 CI에서 실행된 경우와 AWS 키, npm 토큰, GitHub PAT가 있는 개발자 노트북에서 실행된 경우의 사고 등급은 다릅니다. 이번 악성코드는 GitHub, npm, AWS, Vault, Kubernetes 등 다양한 자격 증명을 노린 것으로 분석됐습니다.3
실무적으로는 lockfile 검색만으로 끝내지 말아야 합니다. CI 로그, 캐시 복원 기록, install 시각, runner 권한, 배포 산출물 생성 여부까지 봐야 합니다.
lockfile은 안전벨트가 아니다
많은 팀이 "우리는 lockfile을 쓰니까 괜찮다"고 생각합니다. 하지만 lockfile은 이미 차가 달리고 있는 상황에서는 안전벨트가 아닙니다.
이번 공격의 핵심은 세 단계였습니다. pull_request_target 워크플로가 fork 코드를 신뢰 경계 안에서 실행했고, 공격자는 GitHub Actions 캐시에 악성 pnpm store를 심었습니다. 이후 정상적인 main 브랜치 릴리스가 해당 캐시를 복원했고, runner 메모리에서 OIDC 토큰을 추출해 npm에 악성 버전을 게시했습니다.4
여기서 중요한 해석은 이렇습니다. provenance나 trusted publishing은 "어디서 빌드됐는지"를 증명할 수는 있지만, "그 빌드 환경이 깨끗했는지"까지 보장하지는 않습니다. 실제로 이번 패키지들은 유효한 SLSA provenance를 가진 악성 패키지라는 점에서 더 위험했습니다.5
따라서 점검해야 할 약한 고리는 lockfile 바깥에 있습니다.
자동 머지되는 Renovate/Dependabot, floating range, 오래 살아 있는 CI 캐시, 기본 허용된 postinstall/prepare script, 모든 job에 열려 있는 id-token: write, fork PR에서 실행되는 privileged workflow가 그것입니다.
다음 공격 전에 할 '패키지 화재 훈련'
대응은 즉흥적으로 하면 늦습니다. 팀은 "npm 사고가 났을 때 30분 안에 무엇을 멈추고, 무엇을 확인하고, 무엇을 회전할지"를 미리 정해야 합니다.
최소 플레이북은 이 정도면 됩니다.
새 install과 자동 dependency update를 일시 중지한다.
lockfile diff와 CI install 로그에서 영향을 받은 버전을 찾는다.
node_modules와 빌드 캐시에서router_init.js,@tanstack/setup, 문제 커밋 해시 같은 지표를 확인한다.감염 가능 환경의 npm, GitHub, cloud, Vault, Kubernetes 토큰을 회전한다.
단,
gh-token-monitor같은 persistence/wiper 가능성이 보고된 경우에는 토큰 폐기 전에 해당 daemon을 먼저 제거한다.6GitHub Actions 캐시를 폐기하고 runner 이미지를 새로 만든다.
최근 npm publish 기록과 GitHub Actions 실행 기록을 대조한다.
영구 개선도 필요합니다. 모든 워크플로에 id-token: none을 기본값으로 두고, publish job에만 id-token: write를 줘야 합니다. pull_request_target은 fork 코드를 checkout하거나 실행하지 않는 경우에만 제한적으로 써야 합니다. 가능하면 lifecycle script는 기본 차단하고 필요한 패키지만 allowlist로 허용하는 편이 낫습니다.
물론 내부 npm registry, SBOM, provenance, allowlist가 모두 만능은 아닙니다. 내부 registry는 운영 부담이 크고, SBOM은 실행을 막지 못하며, provenance는 이번처럼 오염된 파이프라인에는 속을 수 있습니다. 하지만 이들은 "탐지와 지연"의 장치로는 가치가 있습니다. 특히 새로 게시된 패키지를 일정 시간 설치하지 않는 cooldown 정책은 6분짜리 공격 창을 무력화할 수 있는 현실적인 방어선입니다.
이번 사건을 TanStack만의 실수로 보면 배울 것이 적습니다. JavaScript 팀이라면 지금 봐야 할 것은 특정 패키지명이 아니라 내 조직의 의존성 실행 경로입니다. 로컬, CI, 캐시, 릴리스 권한이 한 줄로 이어져 있다면 다음 악성 패키지는 "설치"가 아니라 "배포"까지 걸어 들어올 수 있습니다.
참고
1TanStack Npm Packages Compromised Inside The Mini Shai Hulud Supply Chain Attack
2Our response to the TanStack npm supply chain attack
3TanStack npm Packages Compromised in Ongoing Mini Shai-Hulud Supply-Chain Attack
4TanStack npm Supply Chain Attack: Cache Poisoning Postmortem | Lushbinary
6Mini Shai-Hulud Supply-Chain Worm Compromises TanStack, Mistral AI, UiPath, and 160+ npm Packages
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
