
신뢰하던 패키지가 오염됐다는 소식을 들었을 때 가장 어려운 판단은 "우리도 설치했나?"가 아닙니다. 진짜 질문은 "그 코드가 어디에서 실행됐고, 그 환경에 어떤 비밀값이 있었나?"입니다.
이번 Mini Shai-Hulud 공급망 공격에서 TanStack 네임스페이스의 npm 패키지 다수가 악성 버전으로 배포됐고, Mistral 관련 패키지도 영향 목록에 포함됐습니다. Socket은 TanStack 범위에서 84개 npm 패키지 아티팩트가 영향을 받았고, 일부 패키지는 주간 다운로드가 1,200만 회를 넘는다고 분석했습니다.1 문제는 인기 패키지 하나가 뚫렸다는 사실보다, 개발자 노트북·CI 러너·릴리스 워크플로라는 "비밀이 모이는 곳"에서 악성 코드가 실행될 수 있었다는 점입니다.
package-lock보다 먼저 봐야 할 것은 실행 위치다
확인된 기술적 특징은 꽤 구체적입니다. 악성 버전에는 router_init.js라는 약 2.3MB의 난독화된 파일이 추가됐고, GitHub Actions 환경 변수, 임시 디렉터리, 원격 통신, 데몬화 동작 등 CI 환경을 겨냥한 흔적이 보고됐습니다.1 또 일부 패키지에는 GitHub 의존성을 통해 @tanstack/setup을 끌어오고, npm 라이프사이클 훅으로 코드를 실행하는 구조가 포함됐습니다.1
여기서 실무적으로 중요한 해석은 단순합니다. "의존성에 있었는가"와 "실제로 실행됐는가"는 다릅니다. lockfile에 악성 버전이 있어도 설치 스크립트가 차단된 환경이었다면 피해 범위가 달라질 수 있습니다. 반대로 직접 의존성이 아니어도 CI에서 npm install이나 npm ci가 실행됐다면 노출 가능성이 생깁니다.
따라서 영향 평가의 단위는 저장소가 아니라 실행 맥락이어야 합니다.
로컬 개발 머신에서 설치됐는가
GitHub Actions, GitLab CI, Jenkins 같은 CI 러너에서 실행됐는가
프리뷰 배포나 스테이징 빌드에 포함됐는가
릴리스·퍼블리시 워크플로에서 실행됐는가
해당 환경에 npm 토큰, GitHub 토큰, 클라우드 키, Vault/Kubernetes 토큰이 있었는가
이 질문에 답하지 못하면 대응은 둘 중 하나로 흐릅니다. 아무것도 하지 않거나, 모든 비밀값을 무차별 회전시키며 팀을 멈춰 세우는 것입니다.
30분 안에 할 일: 차단, 확인, 회전의 순서를 지켜라
이런 사고에서 첫 30분의 목표는 완벽한 포렌식이 아닙니다. 추가 실행을 막고, 고위험 비밀값부터 보호하며, 나중에 조사할 증거를 남기는 것입니다.
우선 신규 설치를 멈춰야 합니다. CI의 자동 dependency update, nightly build, preview deploy를 일시 중지하고, 문제가 된 버전이 lockfile에 들어오지 못하게 차단합니다. Socket과 Aikido가 공개한 목록에는 @tanstack/react-router, @tanstack/router-core, @mistralai/mistralai, @mistralai/mistralai-azure, @mistralai/mistralai-gcp 등 여러 패키지와 버전이 포함됐습니다.2
다음은 CI 로그 확인입니다. "해당 버전이 존재한다"보다 "그 시각에 설치가 돌았다"가 더 중요합니다. npm install 로그, 캐시 복원 로그, postinstall/prepare 실행 흔적, 외부 네트워크 연결 기록을 봐야 합니다.
그다음 고위험 비밀값부터 회전합니다. 우선순위는 대체로 npm publish 권한, GitHub PAT 및 OIDC 신뢰 설정, 클라우드 장기 키, Vault 토큰, Kubernetes 서비스 계정 순입니다. 모든 키를 동시에 바꾸는 것이 이상적일 수 있지만, 현실에서는 서비스 장애와 조사 지연을 부릅니다. "실행된 환경에 있었던 비밀"부터 바꾸는 편이 더 빠르고 정확합니다.
마지막으로 정리보다 보존이 먼저입니다. 감염 의심 러너 이미지, CI 로그, lockfile, npm 캐시, 워크플로 파일을 지우기 전에 복사해 둬야 합니다. 너무 빨리 청소하면 원인과 lateral movement 여부를 잃습니다.
Trusted Publisher는 방패지만, 만능 서명은 아니다
이번 사고가 불편한 이유는 npm 토큰 유출만의 문제가 아니기 때문입니다. Socket이 전한 TanStack 사후 분석에 따르면, 공격은 pull_request_target 패턴, GitHub Actions 캐시 오염, 러너 프로세스에서의 OIDC 토큰 추출 등이 얽힌 체인으로 설명됩니다. TanStack 측은 npm 토큰이 탈취된 것이 아니라, OIDC trusted publisher 경로가 악용됐다고 봤습니다.1
이 지점이 위협 모델을 바꿉니다. 과거에는 "npm 토큰을 저장하지 않으면 안전하다"가 꽤 강한 주장처럼 들렸습니다. 이제는 "릴리스 워크플로 안에서 공격자 코드가 실행될 수 있는가"가 더 중요해졌습니다.
StepSecurity는 이번 공격을 유효한 SLSA provenance attestation을 동반한 자기 전파형 npm 웜 사례로 설명했습니다.3 이 표현을 그대로 받아들이기보다, 실무적 결론만 취하면 됩니다. 출처 증명은 "어디서 빌드됐는지"를 보여줄 수 있지만, 그 빌드 환경이 이미 오염됐다면 악성 결과물도 그럴듯한 출처를 가질 수 있습니다.
그래서 필요한 통제는 더 구체적이어야 합니다. 모든 워크플로에 id-token: write를 열어두지 말고, 실제 publish job에만 부여해야 합니다. pull_request_target은 외부 기여자 코드와 섞일 때 특히 조심해야 합니다. GitHub Actions 캐시는 포크와 베이스 브랜치 사이의 신뢰 경계를 넘지 않도록 설계해야 합니다. 서드파티 액션은 태그보다 커밋 SHA로 고정하는 편이 낫습니다.
다음 사고 전까지 만들어야 할 '의존성 화재훈련'
이번 사건을 "TanStack을 썼는가"로만 소비하면 다음 공격에서도 같은 혼란이 반복됩니다. 더 나은 질문은 "우리 팀은 신뢰하던 패키지가 오염됐을 때 30분 안에 무엇을 멈출 수 있는가"입니다.
최소한 다음 항목은 문서가 아니라 실제로 테스트돼야 합니다. 누가 CI를 멈출 권한을 갖는가. 어떤 저장소가 npm publish 권한을 갖는가. 어떤 환경에 어떤 비밀값이 들어 있는가. 설치 스크립트를 허용하는 패키지는 무엇인가. lockfile 롤백은 몇 분 안에 가능한가. 새로 발행된 패키지를 일정 시간 보류하는 정책을 적용할 수 있는가.
반론도 있습니다. 설치 스크립트 차단, release age 정책, OIDC 권한 축소는 개발 속도를 늦출 수 있습니다. 네이티브 모듈이나 빌드 도구는 install hook이 필요하기도 합니다. 하지만 이 트레이드오프는 "보안 대 생산성"이 아니라 "평소의 약간의 마찰 대 사고 당일의 전면 정지"에 가깝습니다.
TanStack, Mistral, UiPath, SAP 관련 패키지까지 이어진 Mini Shai-Hulud 흐름은 npm 생태계의 예외적 사고라기보다 앞으로 더 자주 볼 형태에 가깝습니다. 프론트엔드 팀, 플랫폼 팀, DevOps 팀이 지금 신경 써야 할 기준은 하나입니다. 의존성 사고 대응을 취약점 패치 프로세스가 아니라, 비밀값과 빌드 권한을 다루는 인시던트 대응 프로세스로 격상했는가.
참고
2Mini Shai-Hulud Is Back: npm Worm Hits over 160 Packages, including Mistral and Tanstack
3Mini Shai-Hulud Is Back: A Self-Spreading Supply Chain Attack Compromises TanStack npm Packages
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.
