검색
3분 읽기

TanStack npm 공급망 공격 — 42개 패키지가 털린 6분의 기록

GitHub Actions의 pull_request_target, 캐시 포이즈닝, OIDC 토큰 탈취가 만나면
#보안 #npm #GitHub Actions #공급망 #DevOps

핵심 요약

  • 2026년 5월 11일 19:20–19:26 UTC (단 6분), TanStack의 42개 패키지에 84개 악성 버전이 발행됨
  • 공격은 세 취약점의 체이닝: pull_request_target Pwn Request + GitHub Actions 캐시 포이즈닝 + 러너 메모리에서 OIDC 토큰 탈취
  • 외부 연구자 carlini가 20분 만에 발견, Socket.dev가 곧이어 확인
  • 악성 코드는 AWS/GCP/K8s/Vault/GitHub/SSH 자격증명을 긁어 Session 메신저 네트워크로 유출
  • 원문(영문): TanStack 공식 포스트모템

무너지는 npm 패키지 더미 일러스트


어제 자고 일어났더니 타임라인이 빨갰다.

TanStack — React Query, TanStack Router, TanStack Table — 우리가 매일 쓰는 그 라이브러리들이 한 시간 사이 42개 패키지에 악성 코드를 뿌렸다. 5월 11일, 한국 시간으로는 자정쯤이었다.

OIDC 트러스티드 퍼블리셔(trusted publisher)라는, API 토큰 없이 GitHub Actions가 직접 npm에 publish하는 가장 안전하다고 여겨졌던 방식이 정확히 그 신뢰 때문에 뚫렸다.


한 줄로 요약하면

“각각은 평범한 위험이지만, 셋이 합쳐지면 치명적이었다.”

TanStack 팀이 포스트모템에서 한 말이다. 어느 하나만 막혔어도 공격은 실패했을 거다. 셋이 동시에 열려 있었다.


공격은 세 단계로 체이닝됐다

세 취약점 체이닝 다이어그램

1️⃣ pull_request_target — 포크 코드가 시크릿을 보는 마법

bundle-size.yml 워크플로우가 pull_request_target 트리거를 쓰면서 PR이 가리키는 fork 브랜치의 코드를 체크아웃했다. 이 패턴이 그 유명한 “Pwn Request”다.

GitHub의 pull_request_target는 fork PR에서도 메인 레포의 시크릿에 접근 가능한 상태로 워크플로우가 돈다. 안전하게 쓰려면 PR 코드를 절대 실행하면 안 되는데, TanStack은 빌드를 위해 실행했다. 이 순간 공격자의 코드가 TanStack의 권한으로 실행됐다.

2️⃣ 캐시 포이즈닝 — 신뢰 경계가 없었다

GitHub Actions의 캐시는 기본적으로 PR과 main 브랜치 사이에 공유된다. 공격자는 pnpm-store 캐시에 악성 의존성을 심었다. 캐시 키는 평범했다:

Linux-pnpm-store-6f9233a50def742c09fde54f56553d6b449a535adf87d4083690539f49ae4da11

이게 며칠 뒤 main 브랜치의 release.yml이 돌 때 그대로 복원됐다. 공격자 코드가 빌드 의존성에 섞여 들어가는 순간이다.

3️⃣ OIDC 토큰 탈취 — /proc에서 메모리 긁기

npm의 OIDC 트러스티드 퍼블리셔는 단기 토큰을 메모리에 띄운다. 악성 코드는 러너 컨테이너의 /proc/ 파일시스템을 뒤져 이 토큰을 통째로 빼냈다. 그리고 그 토큰으로 직접 npm에 publish 요청을 쐈다.

API 토큰을 안 쓴다고 안전한 게 아니었다. 러너 메모리가 보호되지 않으면 OIDC도 똑같이 털린다.


타임라인 — 23시간의 잠복, 6분의 폭발

UTC 시각사건
2026-05-10 17:16공격자 fork 생성 (zblgg/configuration)
2026-05-10 23:29악성 커밋 추가 (vite_setup.mjs 페이로드)
2026-05-11 ~10:49PR #7378 오픈 → pull_request_target 자동 실행
2026-05-11 11:29포이즈닝된 pnpm 캐시 저장
2026-05-11 19:15release.yml 트리거, 캐시 복원
2026-05-11 19:201차 publish: @tanstack/history 외 41개
2026-05-11 19:262차 publish: 같은 패키지들 패치 버전
2026-05-11 ~19:50carlini가 발견, 이슈 #7383 작성
2026-05-11 ~20:10메인테이너 권한 회수
2026-05-11 21:30캐시 퍼지, 하드닝 PR 머지

23시간 동안 잠복해 있다가 6분 만에 84개 버전을 쏟아냈다. 그리고 20분 뒤에 외부 연구자가 발견했다는 게 이 사건에서 그나마 다행인 부분이다.


뭘 털어 가려고 했나

악성 페이로드는 클라우드 자격증명 사냥꾼이었다:

  • AWS IMDS, Secrets Manager 토큰
  • GCP 메타데이터 토큰
  • Kubernetes 서비스 계정 토큰
  • HashiCorp Vault 토큰
  • ~/.npmrc, ~/.git-credentials
  • 환경변수 / gh CLI의 GitHub 토큰
  • SSH 개인키

유출 경로는 Session/Oxen 메신저 네트워크였다. 종단간 암호화 메신저라 C2 서버가 따로 없다. 차단하려면 도메인/IP 차단밖에 없다. 침착하고 영리한 설계다.

만약 CI에서 이 패키지들이 한 번이라도 설치돼서 위 자격증명에 접근했다면, 그 환경은 이미 손상된 거다. 키 로테이션이 필수다.


우리가 배워야 할 것

이 사건을 보면서 정리한 체크리스트:

워크플로우 레벨

  • pull_request_target 쓰는 워크플로우 전부 감사하기. PR 코드를 절대 실행하지 말 것
  • 실행해야 한다면 repository_owner 가드 추가
  • 서드파티 액션은 commit SHA로 핀, floating tag(@v3) 금지

캐시 / 시크릿 레벨

  • PR과 main 사이에 캐시를 공유하지 않는 정책 검토
  • OIDC 트러스티드 퍼블리셔를 쓰더라도 러너 환경을 신뢰할 수 있는지 다시 생각하기
  • 시크릿 접근 가능한 잡(job)은 코드 실행 잡과 분리

대응 레벨

  • npm 의존성 lock 파일에 optionalDependencies 같은 비정상 필드 모니터링 (carlini가 이걸로 잡았다)
  • Socket.dev, Snyk, Dependabot 같은 공급망 모니터링 도구 도입
  • 자격증명 짧은 TTL 운영. 24시간 이상 가는 토큰 줄이기

마무리

OIDC를 “그래도 API 토큰보다는 안전하지” 하고 쓰던 사람으로서, 이 사건은 한 대 얻어맞은 느낌이었다.

안전한 메커니즘이 안전하지 않은 환경 위에 올라가면 결국 같이 무너진다. 메커니즘이 아니라 환경 전체를 봐야 한다. 워크플로우 한 줄이 시크릿 전부를 노출시킬 수 있다는 걸 잊지 말자.

TanStack 팀이 공개한 포스트모템은 모든 디테일을 다 까 보여준 모범 사례다. 같은 입장이었으면 나는 이렇게 투명하게 못 했을 거 같다. 존경.

원문 전문은 여기서 읽을 수 있다 → TanStack: npm Supply-Chain Compromise Postmortem

— 2026.05.12

링크가 복사되었습니다

댓글