TanStack NPM 공급망 공격 사후 분석 (Postmortem)
Postmortem: TanStack NPM supply-chain compromise
TL;DR Highlight
2026년 5월 11일 TanStack의 42개 npm 패키지가 GitHub Actions cache poisoning과 OIDC 토큰 탈취를 조합한 공격으로 악성 버전이 배포됐으며, 공격 벡터와 대응 과정을 상세히 분석한 글이다.
Who Should Read
GitHub Actions로 CI/CD 파이프라인을 운영하거나 npm 패키지를 배포하는 개발자 및 DevOps 엔지니어. 특히 오픈소스 프로젝트에서 외부 PR을 받는 메인테이너라면 반드시 읽어야 한다.
Core Mechanics
- 공격은 2026-05-11 19:20~19:26 UTC, 단 6분 만에 42개 @tanstack/* 패키지의 84개 악성 버전을 npm에 배포하는 데 성공했다. 패키지당 2개 버전이 약 6분 간격으로 올라왔다.
- 공격의 핵심은 세 가지 기법의 조합이다: pull_request_target의 'Pwn Request' 패턴(포크 PR에서 base 저장소 권한으로 워크플로우 실행), GitHub Actions cache poisoning(포크↔base 신뢰 경계를 넘는 캐시 오염), 그리고 OIDC 토큰의 런타임 메모리 탈취.
- 공격자는 먼저 2026-05-10에 TanStack/router 포크를 만들어 'zblgg/configuration'으로 이름을 바꿔 포크 목록 검색을 회피했다. 악성 커밋에는 '[skip ci]' 메시지를 붙여 포크 측 CI 실행을 막았다.
- bundle-size.yml과 labeler.yml이 pull_request_target 트리거를 사용했기 때문에, 첫 기여자 승인 없이 자동 실행됐다. 이 워크플로우가 악성 커밋(vite_setup.mjs, ~30,000줄 번들 페이로드)을 실행하면서 캐시가 오염됐다.
- 오염된 캐시 항목(1.1GB)은 refs/heads/main 범위로 저장됐고, release.yml이 main에 push될 때 그 캐시를 그대로 복원해 사용한다. 공격자는 다음 릴리즈 때 실행될 캐시 키를 미리 맞춰 심어뒀다.
- 배포된 악성 패키지를 npm install/pnpm install/yarn install로 설치하면, optionalDependencies의 prepare 라이프사이클 스크립트로 ~2.3MB의 난독화된 router_init.js가 실행된다.
- 악성 스크립트는 AWS IMDS/Secrets Manager, GCP 메타데이터, Kubernetes 서비스 어카운트 토큰, Vault 토큰, ~/.npmrc, GitHub 토큰, SSH 개인 키 등을 수집하고, Session/Oxen 메신저 파일 업로드 네트워크(filev2.getsession.org 등)로 유출한다. 종단간 암호화라 IP/도메인 차단 외 네트워크 레벨 방어가 어렵다.
- 악성 스크립트는 자기 전파 기능도 있다. 피해자가 npm에서 유지 관리하는 다른 패키지를 registry.npmjs.org 검색 API로 조회한 뒤, 같은 방식으로 재감염시켜 퍼뜨린다. @mistralai/mistralai 패키지도 이 웜으로 인해 감염됐다가 npm에서 내려졌다.
Evidence
- 토큰 폐기 시 주의사항으로, 악성 페이로드가 ~/.local/bin/gh-token-monitor.sh를 systemd user service(Linux) 또는 LaunchAgent(macOS)로 설치해 60초마다 GitHub API로 토큰 유효성을 polling하며, 토큰이 폐기되면(HTTP 40x 응답) rm -rf ~/ 명령을 실행하는 데드맨 스위치가 심어진다는 경고가 공유됐다.
- Trusted Publishing(CI에서 장기 토큰 없이 OIDC로 npm 배포하는 방식)이 보안의 전부가 아니라는 의견이 제기됐다. 로컬 배포 시 2FA가 있던 것과 달리, CI 파이프라인 배포는 두 번째 인증 요소가 없어서 CI가 뚫리면 바로 배포가 가능하다는 구조적 문제다. GitHub 신뢰 모델 외부에 2FA를 추가한 staged publishing이 필요하다는 의견이 나왔다.
- postinstall/prepare 같은 라이프사이클 스크립트가 근본적인 위험이라는 지적이 많았다. bun은 기본적으로 라이프사이클 스크립트를 비활성화하기 때문에 이 공격에 면역이라는 점도 언급됐다. '2026년에도 의존성에서 임의 코드를 기본 실행하는 건 명백한 실수'라는 강한 비판이 있었다.
- GitHub Actions 캐시가 포크 PR과 base 저장소 사이에 공유된다는 점이 두 번째로 대규모 공급망 공격의 원인이 됐다는 지적이 있었다. 릴리즈 파이프라인은 캐시 없이 항상 새로 빌드해야 한다는 교훈이 공유됐고, 'global mutable cache를 브랜치 간에 공유하면서 호출자가 키를 지정하는 구조 자체가 업계 전체의 실패'라는 비판도 있었다.
- npm의 '의존자가 있으면 unpublish 불가' 정책 때문에 대부분의 패키지를 즉시 내릴 수 없어 npm 보안팀에 서버 측 tarball 삭제를 요청해야 했고, 이 과정에서 수 시간 동안 악성 tarball이 설치 가능한 상태로 남아 있었다. 이에 대해 '그냥 deprecated 처리라도 해라'는 반응이 나왔다.
How to Apply
- 오픈소스 프로젝트에서 외부 PR을 받는 경우, pull_request_target 트리거를 사용하는 워크플로우가 있는지 즉시 점검하라. pull_request_target은 포크 PR에서도 base 저장소 시크릿과 캐시에 접근할 수 있으므로, 신뢰할 수 없는 코드를 checkout해서 실행하는 구조가 되면 이번 공격과 동일한 취약점이 생긴다.
- 릴리즈/배포 워크플로우(release.yml 등)는 PR 워크플로우와 캐시를 공유하지 않도록 별도의 캐시 키를 사용하거나, 배포 시 캐시를 사용하지 않고 항상 새로 빌드하도록 설정하라. 이번 공격은 PR 워크플로우가 오염시킨 캐시를 배포 워크플로우가 그대로 복원하는 구조를 노렸다.
- 2026-05-11에 @tanstack/router, @tanstack/router-core 등 영향받은 패키지를 설치한 CI 환경이나 개발 머신이 있다면, 해당 호스트에서 접근 가능한 AWS, GCP, Kubernetes, Vault, GitHub, npm, SSH 크리덴셜을 전부 교체(rotate)해야 한다. 단, 토큰 폐기 전에 데드맨 스위치 스크립트(~/.local/bin/gh-token-monitor.sh, systemd 서비스 또는 LaunchAgent)가 설치됐는지 먼저 확인하라.
- npm 패키지를 사용하는 프로젝트에서 보안을 강화하려면, bun의 기본 설정처럼 라이프사이클 스크립트(postinstall, prepare 등)를 기본 비활성화하거나 허용 목록(allowlist)으로 관리하는 방식을 도입하는 것을 고려하라. pnpm은 --ignore-scripts 옵션을 지원하며, 이를 CI 환경에 적용하면 이런 류의 공급망 공격의 실행 경로를 차단할 수 있다.
Terminology
관련 논문
LLM이 TLA+로 실제 시스템을 제대로 모델링할 수 있을까? — SysMoBench 벤치마크
LLM이 TLA+ 명세를 작성할 때 문법은 잘 통과하지만 실제 시스템과의 동작 일치도(conformance)는 46% 수준에 그친다는 걸 체계적으로 검증한 벤치마크 연구로, AI 기반 형식 검증의 현실적 한계를 보여준다.
Natural Language Autoencoders: Claude의 내부 활성화를 자연어 텍스트로 변환하는 기법
Anthropic이 LLM 내부의 숫자 벡터(활성화값)를 직접 읽을 수 있는 자연어로 변환하는 NLA 기법을 공개했다. AI가 실제로 무슨 생각을 하는지 해석하는 interpretability 연구의 새로운 진전이다.
ProgramBench: LLM이 프로그램을 처음부터 다시 만들 수 있을까?
LLM이 FFmpeg, SQLite, PHP 인터프리터 같은 실제 소프트웨어를 문서만 보고 처음부터 재구현할 수 있는지 측정하는 새 벤치마크로, 최고 모델도 전체 태스크의 3%만 95% 이상 통과하는 수준에 그쳤다.
첫 번째 토큰이 이미 알고 있다: Single-Decode Confidence로 Hallucination 탐지하기
LLM이 답변의 첫 토큰을 생성할 때의 확률 분포만 봐도, 10번 샘플링하는 semantic self-consistency와 맞먹는 hallucination 탐지 성능이 나온다.
MOSAIC-Bench:코딩 에이전트의 Compositional Vulnerability 유도 측정
티켓 3장으로 쪼개면 Claude/GPT도 보안 취약점 코드를 53~86% 확률로 그냥 짜준다.
LLM의 거절(Refusal) 동작은 단 하나의 방향(Direction)으로 제어된다
13개의 오픈소스 채팅 모델을 분석했더니, 모델이 유해한 요청을 거절하는 동작이 내부 활성화 공간에서 단 하나의 1차원 벡터 방향으로 인코딩되어 있었다. 이 방향을 제거하면 안전 파인튜닝이 사실상 무력화되므로, 현재 안전 학습 방식이 얼마나 취약한지 보여준다.