# npm 공급망 공격으로부터 보호하는 방법

> Clean Markdown view of GeekNews topic #25183. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=25183](https://news.hada.io/topic?id=25183)
- GeekNews Markdown: [https://news.hada.io/topic/25183.md](https://news.hada.io/topic/25183.md)
- Type: news
- Author: [neo](https://news.hada.io/@neo)
- Published: 2025-12-19T09:51:01+09:00
- Updated: 2025-12-19T09:51:01+09:00
- Original source: [pnpm.io](https://pnpm.io/blog/2025/12/05/newsroom-npm-supply-chain-security)
- Points: 3
- Comments: 1

## Topic Body

- Seattle Times는 **Shai-Hulud 2.0** 공격을 우연히 피했지만, 운이 보안 전략이 될 수 없다는 전제에서 클라이언트 측 방어를 도입함  
- npm의 **Trusted publishing / provenance / 세분화 토큰** 같은 개선은 “배포” 측면을 강화하지만, “설치·업데이트” 시점의 악성 코드 실행은 막지 못한다는 공백이 남아있음  
- pnpm은 npm 레지스트리를 그대로 사용하면서도 **소비(install/update) 단계**에서 악성 패키지 실행을 어렵게 만드는 제어를 추가하는 구조임  
- 파일럿에서 pnpm의 3가지 제어를 적용해 **라이프사이클 스크립트 실행, 최신 릴리즈 즉시 설치, 신뢰도 다운그레이드** 같은 벡터를 각각 막도록 설계함  
- 예외는 실패가 아니라 설계 일부로 보고, 예외를 문서화하면서도 나머지 레이어가 계속 보호하도록 **defense-in-depth** 운영을 목표로 함  
  
---  
### 사건 배경과 전제  
- 2025년 11월 **자기 복제형 npm 웜**이 796개 패키지를 감염시키고 월 1억 3,200만 다운로드 규모에서 확산된 사례가 발생  
- 공격은 **preinstall 스크립트**를 활용해 자격 증명 탈취, 지속성 백도어 설치, 일부 환경에서는 개발 환경 삭제까지 일어남  
- 우리 조직에 영향이 없었던 이유는 강한 방어가 아니라, 공격 기간에 `npm install`/`npm update`를 실행하지 않은 **우연**이었음  
- 뉴스 조직은 신뢰가 핵심이며, 공급망 침해로 고객 데이터·직원 자격 증명·프로덕션 인프라·소스 코드가 노출될 수 있고 복구와 통지 비용이 큼  
  
### 팀과 도입 맥락  
- Seattle Times는 기본 패키지 매니저로 npm을 오래 사용해 왔고, Yarn 실험도 있었지만 정착하지 못했음  
- pnpm을 도입하는 이유는 **레지스트리 수준 개선을 보완하는 클라이언트 측 보안 제어** 때문  
- pnpm은 동일 레지스트리·동일 명령·동일 워크플로우를 사용하는 **drop-in replacement**라 전환 가능성이 높다고 판단  
- 완성된 케이스 스터디가 아니라, 실제 팀이 공급망 보안을 막 시작하며 겪는 문제와 사고 과정을 공유하는 것임  
  
### 클라이언트 측 제어가 필요한 이유  
- npm의 보안 개선으로 **계정 탈취 후 악성 패키지 배포**가 더 어려워지기는 했음  
  - [Trusted publishing](https://docs.npmjs.com/trusted-publishers/) 도입  
  - [provenance attestations](https://docs.npmjs.com/generating-provenance-statements/) 제공  
  - [granular access tokens](https://docs.npmjs.com/about-access-tokens/) 지원  
- 이 개선들이 “배포(publishing)” 측면을 보호하지만, “소비(consuming)” 단계에서 악성 패키지 설치 자체를 막지는 못함  
- `npm install`/`npm update` 시 **lifecycle scripts(preinstall/postinstall 등)** 가 패키지 안전성 평가 이전에 개발자 권한으로 임의 코드를 실행할 수 있는 구조임  
- 스크립트가 npm/GitHub/AWS/DB 자격 증명, 소스 코드, 클라우드 인프라, 파일시스템 전체에 접근 가능함  
- Shai-Hulud 같은 공격은 이 구조를 악용하며, 유지보수자 계정이 탈취되면 악성 스크립트가 **설치 순간** 실행되어 커뮤니티가 감지하기 전에 피해가 발생할 수 있음  
- npm의 배포 측 개선 + pnpm의 소비 측 제어를 **상호 보완적**인 방어로 묶어 “defense-in-depth”로 구성  
  
### 적용한 3가지 레이어  
- 파일럿에서는 서로 다른 공격 벡터를 겨냥한 3개 제어를 함께 사용함  
- 각 제어에는 현실적 예외를 위한 탈출구가 있으며, 실제 환경에서는 예외가 필요하다는 전제를 두고 설계함  
  
#### Control 1: Lifecycle Script Management  
- pnpm은 기본적으로 **lifecycle scripts를 차단**하고 설치는 경고와 함께 진행될 수 있음  
- 경고가 무시될 수 있다는 우려로 `strictDepBuilds: true`를 선택해, 스크립트가 있으면 설치가 **즉시 실패**하도록 강제함  
- 설정 예시는 `pnpm-workspace.yaml`에서 다음 필드로 구성됨  
  - `strictDepBuilds: true`  
  - `onlyBuiltDependencies`: 필요한 빌드 스크립트가 있는 패키지 허용 목록  
  - `ignoredBuiltDependencies`: 불필요한 빌드 스크립트가 있는 패키지 차단(또는 무시) 목록  
- “필요한 스크립트”는 네이티브 확장 컴파일, 플랫폼 의존 라이브러리 링크 같은 동작으로 정의됨  
- “불필요한 스크립트”는 최적화나 선택적 설정으로, 팀의 사용 방식에서는 기능에 영향을 주지 않는 경우로 정의됨  
- 설치 실패를 통해 다음 단계를 강제함  
  - pnpm이 어떤 패키지에 스크립트가 있는지 명확히 알려줌  
  - 스크립트 동작을 조사하고 이해  
  - 허용/차단을 사람 판단으로 의식적으로 결정하고 문서화  
- pnpm 팀이 v11에서 `strictDepBuilds: true`를 기본값으로 고려 중이며 allow/deny 문법의 이름 개선도 검토 중  
  
#### Control 2: Release Cooldown  
- 최근에 배포된 버전은 일정 쿨다운 기간 동안 설치를 막아, 커뮤니티가 악성 패키지를 탐지·제거할 시간을 벌어줌  
- 설정 예시는 `pnpm-workspace.yaml`에서 다음 필드로 구성됨  
  - `minimumReleaseAge: &lt;duration-in-minutes&gt;`  
  - `minimumReleaseAgeExclude`: 긴급 핫픽스 등 예외 허용 목록  
- “최신이 최선”이라는 습관을 버리고, 공급망 관점에서는 **조금 오래된 것이 더 안전**할 수 있다는 사고로 전환이 필요  
- 2025년 9월 공격( debug, chalk 포함 16개 패키지)에서 약 2.5시간 내 제거, 2025년 11월 Shai-Hulud 2.0은 약 12시간이 걸렸음  
  - [September 2025 npm supply chain attack 분석](https://www.wiz.io/blog/widespread-npm-supply-chain-attack-breaking-down-impact-scope-across-debug-chalk)  
  - [Shai-Hulud 2.0 분석](https://securitylabs.datadoghq.com/articles/shai-hulud-2.0-npm-worm/)  
- 조직의 리스크 허용도에 따라 쿨다운은 시간/일/주 단위가 될 수 있으며, 어떤 형태든 해당 공격을 막았을 것  
- 조직이 원래도 항상 최신을 쓰지 못하는 현실과 잘 맞아, 쿨다운이 업무를 크게 방해하지 않음  
- 보안 패치나 치명적 버그처럼 꼭 필요할 때는 검토 후 예외로 풀 수 있음  
  
#### Control 3: Trust Policy  
- 이전 버전보다 약한 인증으로 배포된 버전이 나타나면 설치를 차단  
- 유지보수자 계정이 탈취되어 공식 CI/CD가 아닌 공격자 머신에서 배포되는 경우를 신호로 본다고 설명함  
- 설정 예시는 `pnpm-workspace.yaml`에서 다음 필드로 구성됨  
  - `trustPolicy: no-downgrade`  
  - `trustPolicyExclude`: CI/CD 마이그레이션 등 예외 허용 목록  
- npm이 패키지 배포에 대해 3단계 신뢰 수준을 추적한다고 설명함(강→약)  
  - **Trusted Publisher**: GitHub Actions + OIDC + npm provenance 기반  
  - **Provenance**: CI/CD에서 서명된 attestation  
  - **No Trust Evidence**: username/password 또는 토큰 기반 배포  
- 새 버전이 이전보다 신뢰 수준이 낮아지면 설치가 실패  
- 2025년 8월 [s1ngularity 공격](https://www.wiz.io/blog/s1ngularity-supply-chain-attack)에서 공격자가 CI/CD 접근 없이 로컬에서 악성 버전을 배포해 provenance가 없었던 사례에서는 이 제어가 설치를 막았을 것  
- 합법적 다운그레이드 가능 사례로 새 유지보수자 합류, CI/CD 마이그레이션, CI/CD 장애 중 수동 핫픽스 등을 들고, 조사 후 예외 목록에 추가  
- 이 기능이 2025년 11월 pnpm에 추가된 **신규 기능**이며, 실제로 합법적 다운그레이드가 얼마나 자주 발생할지는 학습 중  
  
### 레이어 조합 동작 예시: React 취약점 패치  
- 2025년 12월 공개된 React Server Components의 **치명적 취약점** 패치를 즉시 적용해야 하는 시나리오의 경우  
  - [React Server Components 취약점 공지](https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components)  
- 일반적으로는 쿨다운이 “방금 배포된 버전 설치”를 막지만, 치명적 보안 패치라 기다릴 수 없음  
- 이때 `minimumReleaseAgeExclude`에 특정 React 버전을 추가하되, 취약점 공지와 패치의 정당성을 검토한 뒤 예외를 적용  
- 예외를 적용해도 다른 레이어가 계속 보호함  
  - React는 일반적으로 lifecycle scripts가 없으므로, 패치 버전에 스크립트가 생기면 즉시 의심 신호가 되며 차단될 수 있음  
  - 공격자가 자격 증명을 탈취해 로컬에서 “패치”를 배포하면 신뢰 수준 다운그레이드로 차단될 수 있음  
- 예외를 “보안 실패”가 아니라, 한 레이어를 우회하더라도 다른 레이어가 남아 **단일 실패 지점이 사라지는 설계**  
  
### 파일럿 적용 결과  
- 백엔드 서비스 하나에 3개 제어를 모두 적용해 PoC를 진행함  
- 조사·이해·접근 정의까지 총 준비 시간이 몇 시간 수준이었음  
- pnpm이 lifecycle scripts가 있는 패키지 3개를 식별  
  - **esbuild**: CLI 시작을 밀리초 단위로 최적화하지만, 팀은 JS API만 사용하므로 불필요하다고 판단함  
  - **@firebase/util**: 클라이언트 SDK 자동 구성인데, 팀은 서버 SDK만 사용하므로 불필요하다고 판단함  
  - **protobufjs**: 스키마 호환성 검사인데, 전이 의존성으로만 사용되며 팀 사용 사례에선 불필요하다고 판단함  
- 문서 확인과 스크립트 분석(AI로 스크립트 해석 보조 포함)을 거쳐, 세 스크립트 모두 팀 사용 사례에는 필요 없다고 결론 내리고 차단함  
- 기능 영향은 없었음  
- 마찰(friction)은 의도된 기능이며, 환경에서 실행되는 코드를 **묵시적으로 신뢰하지 않게 만드는 강제 장치**  
- 새 의존성에 스크립트가 있을 경우, 검토 및 문서화에 약 15분 정도 소요될 것으로 예상  
  
### 운영하면서 얻은 학습  
- client-side 레이어 + npm의 publishing-side 개선 조합으로 **defense-in-depth가 실제로 작동**한다고 체감  
- 예외를 적용하더라도 다른 레이어가 남아 있어 예외에 대한 불안이 줄어듦  
- “편의 우선”에서 “보안 우선”으로의 **정신 모델 전환**에 시간이 필요하지만, 익숙해지면 자연스럽게 느껴짐  
- 전담 보안팀이 없는 중간 규모 조직에서도 실용적으로 적용 가능  
- trust policy는 도입된 지 몇 주밖에 안 된 기능이라, 합법적 다운그레이드 빈도와 운영 감각을 더 학습해야 함  
- 가까운 시일 내 다른 코드베이스로 확장할 계획이며, 의존성 그래프가 다른 애플리케이션에서의 데이터가 더 쌓일 것  
  
### 다른 팀을 위한 적용 팁  
- 한 프로젝트에서 먼저 시작해 워크플로우와 마찰 지점을 학습하는 방식을 권장  
- lifecycle scripts, release cooldown, trust downgrade 모두에서 예외가 필요할 수 있으므로 **처음부터 예외를 전제로 설계**할 것  
- 경고 기반보다 설치 실패로 강제하는 `strictDepBuilds: true`를 첫날부터 쓰는 전략을 추천  
- 모든 예외를 문서화해 감사 흔적을 남기고, 향후 정리하기 쉽게 만들 것  
- 한 레이어 예외가 다른 레이어의 보호를 남긴다는 점을 기억할 것

## Comments



### Comment 47994

- Author: bichi
- Created: 2025-12-19T11:45:42+09:00
- Points: 1

pnpm! pnpm! pnpm ! 역시 믿고 있다고
