본문으로 건너뛰기
피드

TanStack npm 패키지 42개가 털린 공급망 공격 사후분석

security 약 14분

TanStack이 2026년 5월 11일 6분 동안 42개 패키지에 걸쳐 84개 악성 버전이 배포된 npm 공급망 사고의 사후분석을 공개했다. 공격자는 pull_request_target, GitHub Actions 캐시 오염, OIDC 토큰 메모리 추출을 이어 붙여 npm 토큰 없이도 패키지를 배포했다. 설치한 사용자는 설치 호스트의 클라우드·깃허브·엔피엠·에스에스에이치 자격증명을 교체해야 하는 수준의 사고다.

  • 1

    공격자는 TanStack 패키지 42개에 악성 버전 84개를 배포했고, 외부 연구자가 약 20분 만에 탐지했다.

  • 2

    핵심 체인은 pull_request_target 실행, 액션 캐시 오염, 릴리스 워크플로의 OIDC 토큰 메모리 추출이었다.

  • 3

    악성 페이로드는 설치 시점에 실행되어 클라우드 키, 쿠버네티스 토큰, 볼트 토큰, 깃허브 토큰, 엔피엠 설정, 에스에스에이치 키를 수집하려 했다.

  • 4

    엔피엠 토큰 자체가 탈취된 것은 아니지만, 감염 버전을 설치한 환경은 잠재적으로 침해된 것으로 봐야 한다.

  • TanStack이 npm 공급망 사고 사후분석을 공개함. 규모가 꽤 큼
    • 2026년 5월 11일 19:20~19:26 UTC 사이, 공격자가 @tanstack/* npm 패키지 42개에 악성 버전 84개를 올림
    • 패키지당 악성 버전이 2개씩 올라갔고, 두 배포 사이 간격은 대략 6분
    • 외부 연구자인 StepSecurity의 ashishkurmi가 약 20분 만에 공개 이슈로 탐지했고, Socket.dev도 전화를 걸어 상황을 확인해줌

⚠️주의

> 2026년 5월 11일에 영향받은 TanStack 패키지 버전을 설치한 환경은 ‘그냥 찝찝함’ 수준이 아니라 잠재적 침해 호스트로 봐야 함. AWS, GCP, Kubernetes, Vault, GitHub, npm, SSH 자격증명까지 교체 권고가 나왔음.

  • 이번 공격의 핵심은 npm 토큰 탈취가 아니었음. CI 신뢰 경계를 차례로 건너뛴 게 포인트임

    • TanStack은 npm 토큰이 도난당한 증거는 없다고 밝힘
    • npm publish 워크플로 자체가 직접 뚫린 것도 아니라고 함
    • 대신 공격자는 GitHub Actions의 PR 실행, 캐시, OIDC trusted publishing을 한 줄로 이어 붙임
  • 공격 체인은 세 조각이 맞물려야만 성립했음

    • 첫째, bundle-size.yml이 pull_request_target 이벤트에서 포크 PR의 merge ref를 체크아웃하고 빌드를 실행함
    • 둘째, 그 실행 중 actions/cache@v5가 pnpm store 캐시를 저장하면서 베이스 저장소 캐시 영역을 오염시킴
    • 셋째, release.yml이 나중에 main push에서 같은 캐시 키를 복원했고, 공격자 바이너리가 릴리스 러너 위에서 실행됨
sequenceDiagram
    participant 공격자포크 as 공격자 포크 PR
    participant 벤치워크플로 as 벤치마크 워크플로
    participant 액션캐시 as GitHub Actions 캐시
    participant 릴리스워크플로 as 릴리스 워크플로
    participant 엔피엠 as npm 레지스트리

    공격자포크->>벤치워크플로: 포크 코드가 pull_request_target에서 실행됨
    벤치워크플로->>액션캐시: 오염된 pnpm store를 정상 캐시 키로 저장
    액션캐시->>릴리스워크플로: main 릴리스 실행 때 오염 캐시 복원
    릴리스워크플로->>릴리스워크플로: 러너 메모리에서 OIDC 토큰 추출
    릴리스워크플로->>엔피엠: 토큰으로 악성 패키지 버전 직접 배포
  • pull_request_target 자체가 무조건 악은 아님. 문제는 거기서 신뢰하지 않는 코드를 실행했다는 점임

    • PR에 댓글 달기, 라벨 붙이기 같은 신뢰된 작업에는 쓸 수 있음
    • 그런데 이번 워크플로는 refs/pull/${{ github.event.pull_request.number }}/merge를 체크아웃하고 빌드를 돌림
    • 즉 외부 포크 코드가 베이스 저장소 권한 맥락과 같은 캐시 스코프 안으로 들어온 셈임
  • 특히 actions/cache@v5의 동작이 함정이었음

    • 워크플로 권한을 contents: read로 낮춰도 캐시 저장이 막히지 않음
    • 캐시 저장은 workflow GITHUB_TOKEN이 아니라 러너 내부 토큰을 사용함
    • pull_request_target 실행은 베이스 저장소 캐시 스코프를 쓰고, main의 릴리스 워크플로도 같은 영역에서 캐시를 복원함

중요

> 이번 사고에서 캐시 키는 Linux-pnpm-store-${hashFiles('**/pnpm-lock.yaml')} 형태였고, 공격자는 릴리스 워크플로가 계산할 키를 맞춰 pnpm store를 오염시켰음. 캐시가 성능 최적화가 아니라 공급망 경로가 된 케이스임.

  • 릴리스 워크플로에는 id-token: write가 있었고, 이건 원래 npm OIDC trusted publishing에 필요한 정상 설정임

    • 문제는 오염된 pnpm store에서 공격자 제어 바이너리가 실행됐다는 것
    • 바이너리는 /proc/*/cmdline으로 GitHub Actions Runner.Worker 프로세스를 찾음
    • 이후 /proc//maps와 /proc//mem을 읽어 러너 메모리를 덤프함
    • 메모리 안에서 OIDC 토큰을 뽑아낸 뒤 registry.npmjs.org에 직접 POST 요청을 보냄
  • 이 기법도 완전히 새로운 건 아니었음

    • 2025년 3월 tj-actions/changed-files 침해 때 쓰였던 메모리 추출 기법과 같은 계열임
    • 심지어 원문에 따르면 파이썬 스크립트가 attribution comment까지 포함해 사실상 재사용됨
    • 공격자가 천재적인 신기술을 만든 게 아니라, 공개된 연구와 기존 공격 기법을 조합한 쪽에 가까움
  • 악성 패키지는 설치 시점에 바로 실행되도록 설계됐음

    • 영향받은 버전을 npm install, pnpm install, yarn install하면 optionalDependencies에 들어간 @tanstack/setup이 해석됨
    • 이 의존성은 tanstack/router 포크 네트워크 안의 orphan payload commit을 가리킴
    • prepare lifecycle script가 실행되면서 약 2.3MB짜리 난독화된 router_init.js가 동작함
  • 페이로드가 노린 자격증명 범위가 넓음. 로컬 개발 머신과 CI 둘 다 위험함

    • AWS IMDS와 Secrets Manager, GCP metadata, Kubernetes service-account token, Vault token을 훑음
    • ~/.npmrc, GitHub 토큰, gh CLI, .git-credentials, SSH private key도 수집 대상임
    • 탈취 데이터는 Session/Oxen 메신저 파일 업로드 네트워크로 빠져나가도록 되어 있었음
    • filev2.getsession.org, seed1.getsession.org, seed2.getsession.org, seed3.getsession.org 같은 도메인이 IOC로 공개됨
  • 더 짜증나는 부분은 self-propagation 기능임

    • 피해자의 npm 계정이 유지보수하는 다른 패키지를 registry.npmjs.org/-/v1/search?text=maintainer:로 열거함
    • 이후 같은 주입 방식으로 다시 배포하려고 시도함
    • 그래서 설치 호스트의 npm 권한이 살아 있으면 단일 프로젝트 사고가 다른 패키지로 번질 수 있음
  • TanStack이 확인한 깨끗한 패키지군도 있음

    • @tanstack/query*, @tanstack/table*, @tanstack/form*, @tanstack/virtual*, @tanstack/store는 clean으로 확인됨
    • @tanstack/start 메타 패키지도 clean이지만, @tanstack/start-* 패키지와는 구분해야 함
    • 영향받은 전체 버전 목록은 GitHub Security Advisory GHSA-g7cv-rxg3-hmpx 쪽에 정리됨
  • 공개된 IOC가 꽤 구체적이라 보안팀은 바로 헌팅 가능함

    • 악성 optionalDependencies 항목은 @tanstack/setup: github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c
    • 악성 파일은 패키지 루트의 router_init.js이며 약 2.3MB
    • 2단계 페이로드 URL은 litter.catbox.moe/h8nc9u.js, litter.catbox.moe/7rrc6l.mjs
    • 위조 커밋 신원은 claude [email protected]였고, 실제 Anthropic Claude와 무관한 조작된 no-reply 이메일이라고 명시됨
    • 공격자 계정으로는 zblgg, voicproducoes가 언급됨
  • 대응은 빨랐지만, 사후분석에서 TanStack은 꽤 솔직하게 약점을 인정함

    • 내부 알림이 아니라 외부 제보로 침해를 알게 됨
    • pull_request_target 워크플로가 오래 알려진 위험 패턴인데도 감사되지 않았음
    • third-party action에 @main 같은 floating ref를 쓰는 것도 별도 공급망 리스크였음
    • npm의 dependents가 있는 패키지는 unpublish가 거의 불가능한 정책 때문에, npm security가 서버 쪽에서 tarball을 제거해주길 기다려야 했음
  • OIDC trusted publishing도 이번 사건 이후 재검토 포인트가 됨

    • 장기 npm 토큰을 없애는 방향은 맞지만, 워크플로 안의 어떤 코드 경로든 토큰을 mint할 수 있으면 공격면이 남음
    • TanStack은 짧은 수명의 classic token에 수동 리뷰를 붙이는 방식, 또는 예상치 못한 워크플로 단계의 배포를 감지하는 provenance-source-verification을 검토해야 한다고 적음
    • npm scope에 maintainer가 7명 있었던 점도 같은 blast radius에 대해 자격증명 탈취 타깃이 7개였다는 의미로 지적됨
  • 운도 있었음. 공격자가 더 조용했다면 훨씬 오래 갔을 가능성이 있음

    • 페이로드가 테스트를 깨뜨렸고, 그래서 정상 publish step이 건너뛰어짐
    • 결과적으로 더 깨끗해 보이는 tarball 대신 눈에 띄는 흔적이 남았음
    • 공격자가 테스트를 깨지 않게 만들었다면 몇 시간 더 조용히 배포됐을 수 있다는 게 TanStack의 평가임
  • 아직 닫히지 않은 질문도 많음

    • 실제로 bundle-size.yml의 Setup Tools 단계가 actions/cache@v5를 호출했는지 post-job 로그로 확인해야 함
    • force-push로 사라진 초기 PR head commit에 무엇이 있었는지 GitHub reflog나 지원팀 확인이 필요함
    • 악성 커밋이 포크의 git object store에 어떻게 들어갔는지도 아직 조사 대상임
    • 다른 TanStack 저장소에도 같은 bundle-size.yml 스타일 패턴이 있는지 감사가 필요함
    • 실제로 영향을 받은 버전이 몇 번 다운로드됐는지는 npm 지원을 통해 확인해야 함

기술 맥락

  • 이번 선택의 핵심은 TanStack이 npm OIDC trusted publishing을 쓰고 있었다는 점이에요. 장기 npm 토큰을 저장하지 않으려는 선택 자체는 보안적으로 합리적이지만, 릴리스 워크플로 안에서 공격자 코드가 실행되면 짧은 수명의 토큰도 훔칠 수 있거든요.

  • pull_request_target은 외부 PR에 자동으로 댓글을 달거나 라벨을 붙일 때는 편해요. 그런데 여기서 포크 코드를 체크아웃하고 빌드를 돌리면, 신뢰하지 않는 코드가 베이스 저장소의 캐시 스코프 안으로 들어와요. 이번 사고에서 그 경계가 첫 번째로 무너졌어요.

  • 캐시는 보통 빌드 속도 최적화로만 생각하기 쉬운데, CI에서는 실행 파일과 의존성이 다음 워크플로로 넘어가는 저장소가 돼요. 공격자는 릴리스 워크플로가 복원할 pnpm store 캐시 키를 맞춰 오염시켰고, main 브랜치 릴리스가 그걸 정상 캐시처럼 가져다 썼어요.

  • OIDC 토큰 추출은 러너 프로세스 메모리를 읽는 방식으로 이뤄졌어요. id-token: write가 켜진 워크플로에서는 러너가 필요할 때 토큰을 메모리에 만들기 때문에, 이미 코드 실행권을 얻은 공격자에게는 그 메모리가 배포 권한으로 이어질 수 있어요.

  • 그래서 이 사건의 교훈은 특정 도구 하나를 금지하자는 얘기가 아니에요. 외부 PR, 캐시, 릴리스, 패키지 배포 권한을 각각 따로 안전하다고 보지 말고, 한 단계가 뚫렸을 때 다음 단계로 권한이 전달되는지를 봐야 해요.

이번 사고는 ‘토큰을 저장하지 않으면 안전하다’는 믿음이 얼마나 얇은지 보여준다. CI 러너에서 코드 실행권을 얻으면 캐시와 OIDC 같은 정상 기능도 배포 권한으로 이어질 수 있다.

댓글

댓글

댓글을 불러오는 중...

security

윈도우 11 BitLocker 우회 취약점 ‘YellowKey’ 공개, WinRE 경로가 문제로 지목됨

YellowKey라는 BitLocker 우회 취약점 공개 글이 올라왔고, 작성자는 Windows Recovery Environment에만 있는 특정 구성요소가 보호된 볼륨 접근을 허용한다고 주장한다. 공개 내용은 Windows 11과 Windows Server 2022/2025가 영향권이고 Windows 10은 제외된다고 설명하며, Microsoft 보안 조직과의 공개 조율도 언급한다.

security

해고 직후 정부 DB 96개 삭제 혐의, 내부자 접근권 회수의 무서운 사례

미국 정부 고객을 상대하던 IT 업체에서 해고된 쌍둥이 형제가 몇 분 뒤 정부 정보가 담긴 데이터베이스 96개를 삭제한 혐의를 받고 있다. 기사에는 이들이 이전에도 컴퓨터 범죄 전력이 있었고, 회사 네트워크에서 5,400개 계정 정보를 모아 Python 스크립트로 외부 서비스 로그인을 시도했다는 정황도 나온다.

security

EFF, 국경 전자기기 수색에도 영장이 필요하다고 제4순회항소법원에 주장

EFF와 ACLU 등은 미국 제4순회항소법원에 국경에서 휴대폰·노트북 같은 전자기기를 수색하려면 영장이 필요하다는 의견서를 냄. 사건은 Dulles 공항에서 미국 시민의 휴대폰이 영장 없이 수색된 뒤 형사 사건으로 이어진 사례이며, EFF는 수동 수색과 포렌식 수색 모두 같은 높은 기준을 적용해야 한다고 주장함.

security

안드로이드 17, 내 폰 OS가 진짜인지 직접 보여준다

구글이 안드로이드 17에 OS 검증 기능을 넣는다. 사용자는 기기가 공식 안드로이드 빌드를 돌리고 있는지, 부트로더 상태와 빌드 정보까지 확인할 수 있고, 구글 앱과 API의 정식 배포 여부를 검증하는 공개 원장도 제공된다.

security

마이크로소프트 취약점 공개전이 또 터짐, 이번엔 2건

익명의 공개자가 마이크로소프트 관련 취약점 2건을 추가로 공개했다고 주장했어. 구체적인 기술 분석은 본문에 거의 없지만, 패치 튜즈데이를 앞두고 더 큰 공개를 예고해 윈도우 보안 운영팀 입장에선 신경 써야 할 신호야.