공급망 공격은 대형 라이브러리 문제만이 아니다 — 오늘 바로 실천할 수 있는 방법

발행: (2026년 5월 19일 AM 07:38 GMT+9)
8 분 소요
원문: Dev.to

Source: Dev.to

“Supply Chain Attacks Aren’t Just a Big Library Problem — Here’s What You Can Do Today” 표지 이미지

LuzAramburo


개요

2026년 5월, Shai‑Hulud라는 웜이 @tanstack/react-router를 포함한 42개의 TanStack 패키지를 침해했습니다. 이 웜은 약 3시간 동안 활동했으며, 그 기간 동안 의존성을 설치한 모든 사람에게 영향을 미쳤습니다.

“재미있는 사실 1”
@tanstack/react-router만 해도 주당 1,270만 건의 다운로드를 기록합니다 → 3시간 동안 약 22만 5천 건이 다운로드된 셈이죠. 이번 공격으로 총 42개의 @tanstack/* 패키지가 영향을 받았습니다.

공급망 공격은 예전엔 “다른 사람의 문제”처럼 느껴졌습니다. 큰 라이브러리가 침해되고, 유지보수자가 이를 고치면 끝이었죠. Shai‑Hulud 웜은 그 인식을 바꾸었습니다. 피해자가 관리하는 모든 패키지에 자동으로 퍼져, 일반 개발자들을 원치 않는 악성코드 배포자로 만들었습니다.

무슨 일이 있었나요?

  • 워크플로우가 pull_request_target을 사용했습니다 – 이 옵션은 기본 레포지토리의 신뢰된 권한(시크릿 접근, 공유 빌드 캐시 등)으로 실행됩니다.
  • 벤치마킹을 위해 포크된 코드를 체크아웃하고 실행했습니다. 이 위험한 조합으로 낯선 코드가 레포지토리의 신뢰를 이용해 실행되었습니다.
  • 공격자는 즉시 무언가를 훔칠 필요가 없었습니다. 공유 캐시를 오염시킨 뒤, 정식 릴리스 파이프라인이 몇 시간 뒤에 실행될 때 변조된 캐시를 사용해 TanStack의 유효한 인증 정보로 악성 패키지를 배포했습니다.

핵심 인사이트 – 잘못된 설정이 눈에 띄지 않았습니다. 벤치마킹 목적은 합리적이었지만, pull_request_target + “PR 코드를 실행”이라는 조합이 안전하다고 가정한 것이 실수였습니다. 언제나 위험한 조합이죠.

“재미있는 사실 2”
웜은 ‘데드맨 스위치’를 설치했습니다: 60초마다 훔친 GitHub 토큰으로 api.github.com/user를 폴링하는 백그라운드 서비스. 토큰이 폐기되면(GitHub이 40x 응답) 서비스가 rm -rf ~/를 실행해 사용자의 홈 디렉터리를 삭제합니다. 자격 증명을 폐기하기 전에 모니터 서비스를 비활성화하고 제거해야 했습니다.

좋은 소식

pull_request_target 기반 공격은 공개 레포지토리에서만 가능하며, 여기서만 외부인이 PR을 열 수 있습니다.

나쁜 소식

공격의 다른 부분(워크플로우 간 캐시 오염, OIDC 토큰 과다 권한 부여)은 프라이빗 레포지토리에도 영향을 줄 수 있습니다. GitHub Actions 워크플로우에 유사한 잘못된 설정이 있다면 말이죠.

지금까지는 라이브러리 유지보수자만의 문제처럼 들릴 수 있습니다. 그렇지 않습니다. 수백만 다운로드를 가진 라이브러리를 관리하지 않아도, 잘못된 시점에 npm install만 하면 됩니다. 침해된 패키지가 node_modules에 들어오는 순간, 여러분도 체인의 일부가 됩니다.

유사 사고를 예방하려면?

npm – .npmrc

min-release-age=7
ignore-scripts=true
  • min-release-age=77일 이내에 발행된 패키지를 차단합니다.
  • ignore-scripts=true는 설치 시 라이프사이클 스크립트(preinstall, prepare 등)의 실행을 방지합니다 — 악성 optionalDependency가 이용한 정확한 경로입니다.
  • ⚠️ npm CLI v11이 필요합니다. Node 24로 업그레이드하거나 npm v11을 수동으로 설치하세요.

pnpm (≥ 10.16) – pnpm-workspace.yaml

minimumReleaseAge: 10080  # minutes → 7 days
  • minimumReleaseAge는 기본값이 1일이며, 7일로 설정하면 더 강력하게 제한합니다.
  • 이 설정을 무시하는 버그를 피하려면 pnpm 10.16 이상이 필요합니다.
  • pnpm v10부터는 기본적으로 preinstall/postinstall 스크립트를 실행하지 않으며(ignore-scripts와 동일한 효과).

Yarn Classic (v1)

  • min-release-age 지원되지 않음; 동등한 옵션이 없습니다.
  • yarn install --ignore-scripts 플래그는 사용할 수 있지만, 영구 설정은 불가능합니다.

Yarn Berry (v2+) – .yarnrc.yml

npmMinimalAgeGate: 10080   # minutes → 7 days
enableScripts: false       # ignore‑scripts와 동등
  • ⚠️ Yarn 4.10 이상이 필요합니다. 전역 적용되며 범위를 지정할 방법이 없습니다.

Dockerfile

ENV npm_config_min_release_age=3
  • npm ci.npmrc 설정과 환경 변수(npm_config_*)를 모두 따릅니다.
  • ⚠️ 여전히 Node 24 또는 npm v11이 필요합니다. 그렇지 않으면 ENV 라인이 조용히 무시됩니다.

옵션 A – Docker에서 Node 24로 업그레이드

FROM node:24-alpine
# npm v11이 포함되어 있어 min‑release‑age가 동작합니다
ENV npm_config_min_release_age=3

옵션 B – 현재 Node 버전을 유지하면서 npm v11을 수동 설치

FROM node:22-alpine
RUN npm install -g npm@11
ENV npm_config_min_release_age=3

출처

참고 자료

  • Snyk 블로그 – TanStack npm 패키지 침해
  • TanStack – npm 공급망 침해 사후 분석
  • TanStack Router – 커밋 히스토리
  • npm min-release-age 설정
  • npm + Node 버전 관계 (npm v11이 Node 24에 포함된다는 확인)
  • pnpm minimumReleaseAge 설정
  • pnpm v11 릴리즈 노트 (설정이 기본 1일로 활성화됨)
  • Yarn Berry npmMinimalAgeGate (Yarn 4.10에서 도입)
  • Yarn Berry enableScripts
  • 표지 이미지 – Unsplash
0 조회
Back to Blog

관련 글

더 보기 »