목차
- TanStack npm 공급망 공격의 기술적 구조
- 악성 페이로드 분석: router_init.js의 동작 원리
- 기존 방어 전략이 실패한 지점
- TanStack npm 공급망 공격 즉시 대응 절차
- 다층 방어 체계 구축: 예방적 보안 설정
- GitHub Actions 워크플로우 보안 강화
- 런타임 탐지와 모니터링 전략
- 방어 체계의 한계점과 열린 문제
“npm 생태계는 충분히 안전하고, 유명 패키지는 검증되어 있으니 걱정할 필요 없다”는 인식이 널리 퍼져 있다. 사실 이 전제는 틀렸다. 2026년 5월 11일 19:20~19:26 UTC 사이, 단 6분 만에 42개 @tanstack/* 패키지에 총 84개 악성 버전이 발행되었다. CVE-2026-45321로 등록된 이 취약점은 CVSS v3.1 기준 9.6/10(Critical)이며, TanStack npm 공급망 공격 대응 방법을 모르면 프로덕션 서버의 자격증명이 통째로 유출될 수 있는 심각한 사건이다. 이 글에서는 공격 체인의 기술적 구조를 해부하고, 기존 방어 전략의 한계를 짚은 뒤, 실전에서 적용 가능한 다층 방어 체계를 제안한다.
TanStack npm 공급망 공격의 기술적 구조
이번 공격은 단일 취약점이 아니라 3단계 체인으로 구성된 정교한 공격이다. 공격자는 pull_request_target 워크플로우 오설정, GitHub Actions 캐시 포이즈닝, 러너 프로세스 메모리에서 OIDC 토큰 추출이라는 순차적 취약점을 결합했다. 각 단계가 독립적으로는 제한적 위험이지만, 체인으로 묶이면서 npm 패키지 발행 권한까지 탈취하는 결과를 낳은 것이다.
pull_request_target 워크플로우 오설정
pull_request_target 이벤트는 외부 PR의 코드를 베이스 브랜치 컨텍스트에서 실행하는 GitHub Actions 트리거다. 일반적인 pull_request 이벤트와 달리, 이 트리거는 저장소의 시크릿에 접근할 수 있는 권한을 PR 작성자에게 간접적으로 부여하게 된다. TanStack 저장소에서 이 설정이 충분한 제약 없이 사용되고 있었고, 공격자는 이를 진입점으로 삼았다.
GitHub Actions 캐시 포이즈닝
두 번째 단계에서 공격자는 GitHub Actions의 캐시 메커니즘을 오염시켰다. CI/CD 파이프라인이 의존하는 캐시에 악성 코드를 주입하면, 이후 실행되는 모든 워크플로우가 오염된 캐시를 사용하게 되는 방식이다. 이 기법은 캐시 키 충돌을 유도하거나 캐시 복원 순서를 조작하는 형태로 이루어진다.
OIDC 토큰 추출과 패키지 발행
마지막 단계에서 공격자는 러너 프로세스 메모리에서 OIDC 토큰을 추출했다. 이 토큰으로 npm 레지스트리에 인증한 뒤, 42개 패키지에 각 2개씩 악성 버전을 발행하는 데 성공했다. TanStack Router 보안 권고문에 따르면 전체 공격 윈도우는 6분에 불과했다. 이는 자동화된 스크립트로 대량 발행이 이루어졌음을 시사한다.
이번 공격은 SLSA provenance 기반 검증이 우회된 최초의 실제 사례로 알려져 있다. 공식 빌드 파이프라인 자체가 장악되었기 때문에 provenance 서명이 정상으로 표시되었고, 기존 서명 기반 검증만으로는 탐지가 불가능한 구조였다. SLSA 공식 문서(slsa.dev)에서 이 우회 시나리오에 대한 구체적 대응은 아직 명시되어 있지 않다.
악성 버전에 포함된 router_init.js는 약 2.3MB 크기의 난독화된 스크립트로, install 시점에 자동 실행되도록 설계되어 있다. 이 페이로드의 동작을 이해하는 것이 TanStack npm 공급망 공격 대응 방법의 출발점이 되는 셈이다.
자격증명 수집 범위
페이로드가 수집하는 자격증명의 범위는 광범위하다. AWS IMDS(Instance Metadata Service)와 Secrets Manager, GCP 메타데이터 서버, Kubernetes 서비스 계정 토큰, HashiCorp Vault 토큰, ~/.npmrc에 저장된 npm 인증 토큰, GitHub 토큰, SSH 개인키까지 모두 탈취 대상이다.
수집된 데이터는 Session/Oxen 메신저의 E2E 암호화 네트워크를 통해 유출되기 때문에, 네트워크 모니터링으로 탈취 내용을 식별하기 어려운 구조다. 일반적인 HTTP 기반 C2 서버와 달리 암호화 메신저를 사용한 점은 포렌식 난이도를 의도적으로 높인 것으로 보인다.
dead-man’s-switch 메커니즘
특히 위험한 부분은 dead-man’s-switch 기능이다. 탈취한 토큰이 폐기(revoke)되었음을 감지하면 rm -rf ~/를 실행하여 홈 디렉토리 전체를 삭제하는 로직이 포함되어 있다. 자격증명 로테이션이라는 정상적인 대응 절차 자체가 추가 피해를 유발할 수 있는 것이다. 따라서 영향받은 호스트에서는 자격증명 로테이션 전에 해당 프로세스를 먼저 종료하고, 시스템 격리 후 조치해야 한다.
악성 패키지 식별은 package.json의 optionalDependencies 필드에서 확인할 수 있다:
"optionalDependencies": {
"@tanstack/setup": "github:tanstack/router#79ac49eedf774dd4b0cfa308722bc463cfe5885c"
}
이 패턴이 존재하면 악성 버전에 해당한다. 다음 명령으로 로컬 패키지를 직접 검사하는 방식이 권장된다:
npm pack @tanstack/<package>@<version>
tar -xzf *.tgz
cat package/package.json | grep -A3 optionalDependencies
ls -la package/router_init.js
router_init.js 파일이 존재하거나 optionalDependencies에 위 GitHub 커밋 해시가 포함되어 있다면 즉시 해당 패키지를 제거해야 한다. TanStack Router 이슈 #7383에서 커뮤니티가 보고한 악성 페이로드의 상세 분석을 확인할 수 있다.
42개 `@tanstack/*` 패키지에 각 2개씩 총 84개 악성 버전이 발행되었다. router뿐 아니라 query, table, form 등 TanStack 생태계 전반에 걸쳐 있으므로, `@tanstack/` 스코프 전체를 점검 대상으로 삼아야 한다.
이 공격이 기존 보안 체계를 뚫은 이유를 분석하면, 통상적인 npm 보안 권장사항의 구조적 한계가 드러난다.
lockfile 고정의 한계
lockfile은 의존성 버전을 고정하는 가장 기본적인 방어 수단이다. 그러나 npm install이나 npm update를 실행하면 lockfile이 갱신되면서 악성 버전이 유입될 수 있다. 특히 semver 범위(^, ~)를 사용하는 프로젝트에서는 lockfile 갱신 시점에 악성 버전이 범위 내에 포함되는 경우가 생긴다.
npm audit의 시차 문제
npm audit는 알려진 취약점 데이터베이스에 의존하는데, CVE 등록과 데이터베이스 반영 사이에 시차가 존재한다. 이번 사건에서 악성 버전이 발행된 시점부터 CVE가 등록되기까지의 시간 동안에는 audit가 아무런 경고도 출력하지 않았을 가능성이 높다. 제로데이 공급망 공격에 대한 audit의 탐지 능력은 본질적으로 제한적이다.
서명 기반 검증의 맹점
SLSA provenance나 npm 서명은 “공식 빌드 파이프라인에서 생성되었는가”를 검증한다. 그런데 이번 공격은 공식 파이프라인 자체를 장악했기 때문에 서명이 정상이었다. 빌드 인프라의 무결성이 깨지면 서명 검증은 무력해지는 것이다.
| 방어 수단 | 이번 공격에서의 효과 | 한계 |
|---|---|---|
| lockfile 고정 | npm install 시 우회 가능 | 갱신 시점에 악성 버전 유입 |
| npm audit | CVE 등록 전 탐지 불가 | 제로데이 시차 존재 |
| SLSA provenance | 정상 서명으로 표시됨 | 빌드 인프라 장악 시 무의미 |
| 2FA | 패키지 발행자 보호 | OIDC 토큰 경로는 2FA 우회 |
TanStack npm 공급망 공격 즉시 대응 절차
영향받은 프로젝트에서 취해야 할 즉시 조치는 명확하다. 우선순위에 따라 순차적으로 실행해야 하며, 순서를 바꾸면 dead-man’s-switch에 의한 추가 피해가 발생할 수 있다.
1단계: 악성 프로세스 격리
영향받은 호스트에서 node 프로세스를 먼저 종료한다. dead-man’s-switch가 토큰 폐기를 감지하면 rm -rf ~/를 실행하므로, 자격증명 로테이션보다 프로세스 종료가 선행되어야 하는 점이 핵심이다.
2단계: 의존성 버전 고정과 클린 재설치
모든 @tanstack/* 의존성을 2026-05-11 19:00 UTC 이전 발행 버전으로 고정한다. 그 뒤 node_modules와 lockfile을 삭제하고 클린 재설치를 수행한다. 이때 --ignore-scripts 플래그를 반드시 사용해야 재설치 과정에서 악성 스크립트가 다시 실행되는 것을 차단할 수 있다:
npm config set ignore-scripts true
반드시 악성 프로세스 종료 → 시스템 격리 → 자격증명 로테이션 순서를 지켜야 한다. 순서가 바뀌면 dead-man’s-switch가 작동하여 홈 디렉토리가 삭제될 수 있다. 클라우드 환경이라면 인스턴스 스냅샷을 먼저 확보하는 편이 안전하다.
설치 프로세스가 접근 가능한 모든 자격증명을 즉시 로테이션한다. 범위는 AWS IAM 키, GCP 서비스 계정 키, Kubernetes 서비스 계정 토큰, npm 토큰, GitHub PAT, SSH 키 전체를 포함한다. 단순히 “의심되는” 키만 바꾸는 것이 아니라, 해당 호스트에서 접근 가능했던 모든 시크릿을 대상으로 삼아야 한다.
4단계: 클라우드 감사 로그 검토
영향받은 호스트의 클라우드 감사 로그를 검토하여 비정상적인 API 호출이 있었는지 확인한다. AWS CloudTrail, GCP Cloud Audit Logs에서 공격 윈도우(2026-05-11 19:20~19:26 UTC) 전후 시간대의 이상 활동을 집중적으로 점검해야 한다.
npm ci --ignore-scripts
이 명령은 lockfile에 기록된 정확한 버전만 설치하고, postinstall 등 임의 스크립트 실행을 차단한다. npm install 대신 npm ci를 사용하는 것이 TanStack npm 공급망 공격 대응 방법의 기본 원칙이 되는 셈이다.
다층 방어 체계 구축: 예방적 보안 설정
즉시 대응 이후에는 유사한 공격을 사전에 차단할 수 있는 다층 방어 체계를 구축해야 한다. Node.js 보안 모범 사례 가이드에서는 5가지 핵심 대응을 권장하고 있다.
npm ci와 ignore-scripts의 상시 적용
CI/CD 파이프라인에서 npm install 대신 npm ci를 사용하면 lockfile과 package.json의 불일치 시 설치가 실패한다. 여기에 --ignore-scripts를 결합하면 postinstall 훅을 통한 악성 코드 실행을 원천 차단할 수 있다:
npm ci --ignore-scripts
npm config set ignore-scripts true
다만 ignore-scripts를 전역으로 설정하면 node-gyp 기반 네이티브 모듈(예: bcrypt, sharp)의 빌드가 실패하기도 한다. 이 경우 해당 패키지만 예외로 명시적 빌드를 실행하거나, 사전 빌드된 바이너리를 제공하는 대체 패키지를 사용하는 방법이 있다. 프로젝트별로 .npmrc 파일에 설정을 분리하는 편이 관리하기 용이하다.
dependency review action 적용
GitHub 공급망 보안 가이드에서 권장하는 dependency review action은 PR 단계에서 새로 추가되거나 변경된 의존성의 취약점을 자동으로 검사한다. 취약한 버전이 포함된 PR은 머지를 차단할 수 있어서, lockfile 갱신 시점의 악성 버전 유입을 방지하는 데 효과적이다.
SBOM 기반 의존성 인벤토리
SBOM(Software Bill of Materials)을 생성하면 프로젝트의 직접·간접 의존성 전체를 목록화할 수 있다. 사건 발생 시 영향 범위를 신속하게 파악하는 데 필수적인 자료가 된다. GitHub에서는 저장소 설정에서 SBOM 자동 생성을 활성화할 수 있다.
Dependabot과 secret scanning 활성화
Dependabot 자동 업데이트는 보안 패치가 나오면 PR을 자동으로 올린다. secret scanning은 커밋에 포함된 API 키, 토큰 등을 자동 감지하여 유출을 차단하는 기능이다. 두 기능 모두 GitHub 저장소 설정에서 활성화할 수 있으며, 공급망 공격의 사후 피해를 줄이는 데 기여한다.
`npm ci –ignore-scripts` 상시 사용, dependency review action PR 차단 설정, SBOM 자동 생성 활성화, Dependabot 보안 업데이트 활성화, secret scanning 활성화 — 이 5가지를 CI/CD 파이프라인에 기본으로 적용하면 공급망 공격의 진입 경로 대부분을 차단할 수 있다.
이번 공격의 진입점이 pull_request_target 워크플로우 오설정이었다는 점에서, GitHub Actions 자체의 보안 설정이 방어의 핵심 계층 중 하나다.
pull_request_target 사용 제한
pull_request_target 트리거는 외부 PR의 코드를 베이스 브랜치의 시크릿 접근 권한으로 실행하는 위험한 설정이다. 불가피하게 사용해야 한다면 실행 대상 코드를 actions/checkout으로 PR HEAD가 아닌 베이스 브랜치로 제한하고, PR의 코드를 직접 실행하지 않도록 워크플로우를 설계해야 한다.
가장 안전한 접근은 pull_request_target 대신 pull_request 이벤트를 사용하는 것이다. 시크릿이 필요한 작업은 별도의 수동 승인 워크플로우로 분리하면 외부 PR에 의한 시크릿 노출을 차단할 수 있다.
Actions 캐시 무결성 검증
GitHub Actions 캐시 포이즈닝을 방지하려면 캐시 키에 lockfile 해시를 포함시키고, 캐시 복원 후 의존성 무결성을 재검증하는 단계를 추가하는 방법이 있다. 캐시를 아예 사용하지 않는 것도 선택지지만, 빌드 시간 증가라는 트레이드오프가 존재하므로 프로젝트 규모에 따라 판단해야 한다.
OIDC 토큰 스코프 최소화
npm 패키지 발행에 사용되는 OIDC 토큰의 권한 범위를 최소화해야 한다. 발행 전용 워크플로우를 별도로 분리하고, 해당 워크플로우만 id-token: write 권한을 갖도록 설정하면 다른 워크플로우에서 토큰이 탈취되더라도 발행 권한은 획득할 수 없게 된다.
| 설정 항목 | 권장 값 | 효과 |
|---|---|---|
| pull_request_target | 사용 금지 또는 베이스 브랜치 코드만 실행 | 외부 PR 시크릿 접근 차단 |
| 캐시 키 | lockfile 해시 포함 | 캐시 포이즈닝 난이도 증가 |
| id-token 권한 | 발행 워크플로우에만 부여 | OIDC 토큰 탈취 영향 범위 축소 |
런타임 탐지와 모니터링 전략
예방만으로는 모든 공급망 공격을 차단할 수 없으므로, 런타임 단계에서 이상 행동을 탐지하는 모니터링 계층도 구축해야 한다.
설치 시점 이상 탐지
npm install 또는 npm ci 실행 시 예상치 못한 네트워크 요청이 발생하는지 모니터링하는 것이 첫 번째 방어선이다. 특히 IMDS 엔드포인트(169.254.169.254)로의 HTTP 요청, 알려지지 않은 외부 도메인으로의 아웃바운드 연결, ~/.npmrc나 ~/.ssh/ 디렉토리에 대한 비정상적 파일 읽기 시도를 감시해야 한다.
이번 공격에서 페이로드가 접근한 대상 목록 — AWS IMDS, GCP 메타데이터, Kubernetes 서비스 계정, Vault, .npmrc, GitHub 토큰, SSH 키 — 을 기준으로 모니터링 규칙을 구성하면 유사 패턴의 공격을 조기 탐지할 수 있다.
클라우드 환경 IMDS 접근 제한
AWS에서는 IMDSv2를 강제하고 홉 제한을 1로 설정하면 컨테이너 내부에서의 IMDS 접근을 차단할 수 있다. GCP에서도 메타데이터 서버 접근을 워크로드 아이덴티티로 제한하는 설정이 가능하다. CI/CD 러너가 클라우드 인스턴스에서 실행되는 경우 이 설정은 공급망 공격의 자격증명 탈취 범위를 크게 줄여준다.
패키지 크기 이상 모니터링
이번 악성 페이로드 router_init.js의 크기는 약 2.3MB였다. 일반적인 프론트엔드 유틸리티 패키지에서 단일 파일이 2MB를 넘는 경우는 드물다. CI 파이프라인에서 설치된 패키지의 파일 크기를 검사하고, 이전 버전 대비 급격한 크기 변화가 있을 때 경고를 발생시키는 자동화를 구축하면 난독화된 악성 코드를 조기에 발견할 수 있는 경우가 있다.
npm audit 외에 Socket 같은 도구는 패키지의 행동 패턴(네트워크 요청, 파일시스템 접근, 환경변수 읽기)을 분석하여 의심스러운 의존성을 탐지한다. Node.js 보안 가이드에서도 이러한 도구의 CI 통합을 권장하고 있다. 다만 npm 공식 문서(docs.npmjs.com)의 상세 설정 가이드는 현재 직접 인용이 불가능하므로 각 도구의 공식 저장소를 참조해야 한다.
이번 사례에서 구축된 방어 체계는 공급망 공격의 모든 시나리오를 커버하지 않는다. 구조적으로 해결되지 않은 문제가 남아 있다.
첫째, ignore-scripts는 네이티브 모듈 빌드를 차단하므로 모든 프로젝트에 일괄 적용할 수 없다. bcrypt, sharp, sqlite3 같은 패키지는 postinstall 스크립트가 필수적이어서, 예외 관리 비용이 발생한다. 보안과 호환성 사이의 트레이드오프는 프로젝트마다 다르게 나타난다.
둘째, SLSA provenance가 우회된 이번 사례는 빌드 인프라 자체의 무결성 보장이라는 더 근본적인 문제를 제기한다. 서명 검증이 “빌드 파이프라인이 안전하다”는 전제에 의존하는 한, 파이프라인 장악 공격에는 무력한 것이다. SLSA 공식 문서(slsa.dev)에서도 이 시나리오에 대한 구체적 대응은 아직 명시되어 있지 않다.
셋째, pnpm v11이 도입한 security-by-default 설정(스크립트 기본 차단)은 유망한 접근이지만, npm과 yarn 생태계에서는 아직 기본값이 아니다. pnpm 공식 문서(pnpm.io)의 상세 설정은 직접 인용이 불가능하나, 패키지 매니저 차원의 보안 기본값 강화는 생태계 전체의 과제로 남아 있다.
넷째, TanStack 공식 postmortem(tanstack.com)이 발표되었으나 현재 직접 인용 가능한 상태가 아니다. 공격 타임라인과 대응 과정의 전체 그림은 해당 문서를 참조해야 완성되는 부분이 있다.
결국 npm 공급망 보안은 단일 도구나 설정으로 해결되지 않는다. lockfile 고정, 스크립트 차단, dependency review, SBOM, 런타임 모니터링, 워크플로우 권한 최소화를 계층적으로 조합하고, 각 계층이 실패했을 때 다음 계층이 방어하는 구조가 TanStack npm 공급망 공격 대응 방법의 핵심 원칙이다. Mini Shai-Hulud로 명명된 이번 공격이 GitHub Actions 캐시 포이즈닝과 OIDC 토큰 탈취라는 새로운 공격 벡터를 실증한 만큼, CI/CD 파이프라인 보안 감사와 npm postinstall 보안 정책 재검토가 다음 단계의 과제가 된다.