우리 뉴스룸을 npm 공급망 공격으로부터 보호하는 방법
우리는 Shai-Hulud 2.0 때 운이 좋았습니다.
지난 2025년 11월, 자기 복제형 npm 웜이 796개 패키지를 감염시켰으며, 이 패키지들의 월간 다운로드 수는 132M에 달합니다. 이 공격은 preinstall 스크립트를 사용해 자격 증명을 훔치고, 지속적인 백도어를 설치했으며, 일부 경우에는 개발자 환경 전체를 삭제했습니다. 우리는 영향을 받지 않았습니다—견고한 방어 체계가 있어서가 아니라, 공격 기간 동안 npm install이나 npm update를 실행하지 않았기 때문입니다.
운은 보안 전략이 아닙니다.
우리는 누구인가#
저는 Seattle Times의 수석 소프트웨어 엔지니어 Ryan Sobol입니다. 우리는 수년간 npm을 기본 패키지 관리자로 사용해 왔으며, Yarn을 잠시 실험해 보았지만 자리 잡지 못했습니다. 이제 우리는 npm이 도입해 온 레지스트리 수준의 개선을 보완하는 클라이언트 측 보안 제어 기능 때문에 pnpm을 시범 도입하고 있습니다.
특히 요즘 같은 때에 뉴스 조직에게 신뢰는 무엇보다 중요합니다. 공급망이 침해되면 고객 데이터, 직원 자격 증명, 프로덕션 인프라, 소스 코드가 노출될 수 있으며—모두 복구하는 데 몇 주가 걸리고 독자들에게 침해 사실을 통지해야 할 수도 있는 것들입니다. 우리는 이러한 사고가 시간과 비용 면에서 얼마나 큰 대가를 치르게 하는지 잘 알고 있습니다. 그것은 우리가 가고 싶지 않은 길입니다.
우리는 npm을 고수하는 데서 오는 조직적 관성에도 불구하고, pnpm에 실제로 기회가 있다고 생각합니다. 이는 진정한 드롭인 대체품입니다—같은 명령, 같은 워크플로, 같은 레지스트리. 덕분에 이전의 대안들과 달리 전환이 실현 가능합니다.
이 글은 잘 다듬어진 사례 연구가 아닙니다. 이제 막 공급망 보안을 파악하기 시작한 팀의 실제 경험 사례입니다. 우리가 겪고 있는 어려움과 이러한 제어 기능에 대한 우리의 생각이 여러분이 직접 도입을 고려할 때 도움이 될 수 있습니다.
클라이언트 측 제어가 중요한 이유#
npm은 공급망 보안 측면에서 엄청난 진전을 이루었습니다. 신뢰할 수 있는 게시, 출처 증명, 세분화된 액세스 토큰은 모두 메인테이너 계정을 탈취한 뒤 악성 패키지를 게시하기를 훨씬 어렵게 만드는 중요한 개선 사항입니다.
하지만 여기에 빈틈이 있습니다: 이러한 레지스트리 개선은 게시 측을 보호합니다. 악성 패키지를 사용하는 것은 막지 못합니다.
여러분이 npm install이나 npm update를 실행하면, 수명 주기 스크립트(예: preinstall 및 postinstall)가 패키지의 안전성이 평가되기도 전에 개발자의 전체 권한으로 인터넷에서 가져온 임의의 코드를 실행합니다. 이러한 스크립트는 자격 증명(npm, GitHub, AWS, 데이터베이스), 소스 코드, 클라우드 인프라, 파일 시스템 전체에 접근할 수 있습니다.
이것이 Shai-Hulud 같은 공격이 악용하는 근본적인 취약점입니다. 이러한 레지스트리 개선이 있더라도 정당한 메인테이너의 계정이 탈취되면, 공격자는 커뮤니티가 침해를 감지하기 전에 설치 즉시 실행되는 악성 수명 주기 스크립트가 포함된 버전을 게시할 수 있습니다.
그래서 우리는 양쪽 모두에 방어가 필요하다고 느꼈습니다: npm의 개선은 악성 패키지를 게시하기 어렵게 만들고, pnpm의 클라이언트 측 제어는 이를 사용하기 어렵게 만듭니다. 이러한 접근 방식은 경쟁 관계가 아니라 상호 보완적입니다. pnpm은 npm의 레지스트리를 사용하며 npm의 모든 보안 개선의 혜택을 누리면서 클라이언트 측에 추가 보호 계층을 더합니다.
이것이 심층 방어입니다.
우리가 사용하는 세 가지 계층#
시범 도입에서 우리는 함께 동작하는 세 가지 pnpm 보안 제어를 사용하고 있습니다. 각 제어는 서로 다른 공격 벡터를 다루며, 각각 정당한 예외를 위한 탈출구를 갖추고 있습니다. 우리는 처음부터 그런 예외가 필요하리라는 것을 알고 있었습니다—현실은 복잡하니까요.
제어 1: 수명 주기 스크립트 관리#
우리가 pnpm을 고려한 주요 이유 중 하나는 pnpm이 기본적으로 수명 주기 스크립트를 차단한다는 사실을 알게 되었기 때문입니다. 다른 패키지 관리자와 달리, pnpm은 패키지의 임의 코드를 암묵적으로 신뢰하고 실행하지 않습니다.
실제로 패키지에 preinstall 또는 postinstall 스크립트가 있으면 pnpm은 이를 차단하지만 설치는 경고와 함께 계속 진행됩니다. 이것만으로도 상당한 보호를 제공합니다—명시적으로 허용하지 않는 한 악성 스크립트는 실행되지 않습니다. 하지만 특히 설치가 성공한 것처럼 보이기 때문에 경고를 너무 쉽게 무시할 수 있다는 점이 우려되었습니다. 그래서 우리는 strictDepBuilds: true로 더 엄격하게 제어하고자 했습니다:
strictDepBuilds: true
onlyBuiltDependencies:
- package-with-necessary-build-scripts
ignoredBuiltDependencies:
- package-with-unnecessary-build-scripts
여기서 "필요한"이란 동작하기 위해 수명 주기 스크립트가 실제로 필요한 패키지를 의미합니다—소스에서 컴파일하는 네이티브 확장이나 플랫폼별 라이브러리에 링크하는 데이터베이스 드라이버 같은 것들입니다. 그리고 "불필요한"이란 선택적인 최적화이거나, 우리 사용 사례에서 패키지의 동작 여부에 영향을 주지 않는 설정 단계인 스크립트를 의미합니다.
이제 strictDepBuilds: true를 사용하면 수명 주기 스크립트를 만났을 때 설치가 즉시 실패하므로, 우리는 다음을 해야 합니다:
- 어떤 패키지에 수명 주기 스크립트가 있는지 파악하기—pnpm이 정확히 어떤 패키지인지 알려줍니다
- 각 스크립트가 무엇을 하는지 조사하기—자체 완결된 preinstall 또는 postinstall 스크립트를 생성형 AI에 넣어 해석을 요청하는 것만큼 간단할 수 있습니다
- 사람의 판단으로 허용할지 차단할지에 대해 의식적이고 문서화된 결정을 내리기
우리 팀에게 이는 나중에 문제를 발견할 가능성 대신 미리 신중한 선택을 하도록 보장합니다.
참고: pnpm 팀은 v11에서 strictDepBuilds: true를 기본 동작으로 만드는 것을 고려하고 있으며, 이러한 제어를 실제로 도입한 팀들의 피드백을 바탕으로 허용/거부 구문에 더 명확한 이름을 붙이는 방안도 검토하고 있습니다.
제어 2: 릴리스 대기 기간#
이 제어는 대기 기간 내에 게시된 패키지 버전의 설치를 차단합니다. 악성 패키지가 여러분의 환경에 도달하기 전에 커뮤니티가 이를 감지하고 제거할 시간을 주자는 것이 핵심입니다.
minimumReleaseAge: <duration-in-minutes>
minimumReleaseAgeExclude:
- package-with-critical-hotfix@1.2.3
우리의 사고방식 전환: 우리는 "최신이 최고"라는 생각을 멈추도록 스스로를 다시 훈련해야 했습니다. 우리가 배우고 있는 것은 공급망 보안 관점에서 항상 그렇지는 않다는 것입니다—조금 더 오래된 것이 종종 더 안전할 수 있습니다. 일정 기간 공개되어 있던 패키지는 커뮤니티와 보안 연구자들에게 잠재적인 문제를 감지할 시간을 줍니다.
최근 공격들을 살펴보면, 악성 패키지는 다양한 기간 내에 감지되고 제거되었습니다. 지난 2025년 9월 npm 공급망 공격은 debug, chalk 및 기타 16개 패키지를 감염시켰고 약 2.5시간 만에 제거된 반면, Shai-Hulud 2.0 (2025년 11월)은 약 12시간이 걸렸습니다. 모든 공격은 다르고 복구 일정도 제각각이지만, 적절한 대기 기간은 조직의 위험 허용 범위에 따라 달라집니다—시간, 일, 주 단위가 될 수 있습니다. 어느 쪽이든 대기 기간이 있었다면 이러한 공격을 막을 수 있었을 것입니다.
우리가 받아들인 절충: 조직의 규모와 우선순위를 고려할 때, 최선을 다하더라도 우리는 항상 패키지의 절대 최신 버전을 사용하지는 않습니다. 그래서 이 대기 기간 정책은 우리의 현실을 방해하기보다는 오히려 더 잘 맞습니다. 정말로 더 새로운 버전이 필요할 때(중요한 보안 패치, 치명적인 버그), 검토 후 일시적으로 예외 처리할 수 있습니다.
제어 3: 신뢰 정책#
이 제어는 패키지 버전의 인증 수준이 이전에 게시된 버전보다 약할 때 설치를 차단합니다—이는 종종 공격자가 메인테이너 자격 증명을 탈취하여 공식 CI/CD 파이프라인 대신 자신의 기기에서 게시했다는 신호입니다.
trustPolicy: no-downgrade
trustPolicyExclude:
- package-that-migrated-cicd@1.2.3
동작 방식: npm은 게시된 패키지에 대해 세 가지 신뢰 수준을 추적합니다(강한 순서대로):
- 신뢰할 수 있는 게시자: OIDC 토큰과 npm 출처 증명을 사용하여 GitHub Actions를 통해 게시됨
- 출처 증명: CI/CD 시스템의 서명된 증명
- 신뢰 증거 없음: 사용자 이름/비밀번호 또는 토큰 인증으로 게시됨
새 버전의 인증 수준이 이전 버전보다 약하면 설치가 실패합니다. 예를 들어 v1.0.0은 신뢰할 수 있는 게시자로 게시되었지만 v1.0.1은 기본 인증으로 게시되었다면, pnpm은 v1.0.1을 차단합니다.
2025년 8월 s1ngularity 공격에서 공격자들은 메인테이너 자격 증명을 탈취하여 자신의 기기에서 악성 버전을 게시했습니다. 공격자들에게는 CI/CD 접근 권한이 없었기 때문에 악성 버전에는 출처 증명이 없었습니다—명백한 신뢰 다운그레이드입니다. 이 제어가 있었다면 설치를 차단했을 것입니다.
신뢰 다운그레이드가 정당할 수 있는 경우: 아직 출처 증명을 설정하지 않은 새 메인테이너, CI/CD 시스템 마이그레이션, CI/CD가 중단된 동안 수동으로 게시된 긴급 핫픽스. 이러한 경우 신뢰 수준이 낮아진 이유를 조사하고, 안전한지 확인한 다음 trustPolicyExclude에 추가합니다.
참고: 이 기능은 2025년 11월에 pnpm에 추가되었으며 꽤 새로운 기능입니다. 실제로 정당한 신뢰 다운그레이드가 얼마나 자주 발생하는지는 아직 알아가는 중입니다.
함께 동작하는 방식: React 사례#
우리는 이러한 제어 중 어느 것도 만병통치약으로 보지 않습니다. 이들은 방어 계층으로 동작합니다—한 가지 제어에 예외를 두어야 할 때도 다른 계층이 계속 우리를 보호합니다.
실제 시나리오를 살펴보겠습니다: 심각한 React 취약점으로, 2025년 12월에 공개되었습니다.
이는 즉각적인 패치가 필요한 심각한 보안 문제였습니다. 평소라면 릴리스 대기 기간 때문에 이렇게 최근에 게시된 패키지 버전을 설치할 수 없었을 것입니다. 하지만 이것은 중요한 보안 패치였습니다—기다릴 수 없었습니다.
이 시나리오에서 계층화된 방어는 다음과 같이 동작합니다:
해야 할 일: 취약점 공개 내용을 검토하고 패치가 정당한지 확인한 후 특정 React 버전을 minimumReleaseAgeExclude에 추가합니다.
여전히 여러분을 보호하는 것:
- 수명 주기 스크립트 관리가 여전히 활성화되어 있습니다—공격자가 React 패치에 악성 수명 주기 스크립트를 주입했다면 차단되었을 것입니다(React에는 보통 수명 주기 스크립트가 없으므로 어떤 스크립트든 즉시 의심스러울 것입니다)
- 신뢰 정책이 여전히 활성화되어 있습니다—공격자가 React의 게시 자격 증명을 탈취하여 자신의 기기에서 악성 "패치"를 푸시했다면, 신뢰 다운그레이드가 차단되었을 것입니다
이것이 우리가 예외는 예상된 것이며 괜찮다고 생각하는 이유입니다. 정당한 이유로 한 가지 제어를 우회하기로 의식적이고 문서화된 결정을 내리더라도, 다른 계층으로부터 여전히 견고한 보호를 받습니다. 단일 장애 지점이 없습니다.
우리에게 실제 심층 방어는 바로 이런 모습입니다.
우리의 시범 도입 경험#
우리는 개념 증명으로 백엔드 서비스 중 하나에 세 가지 보안 제어를 모두 구현했습니다. 총 설정 시간: 조사하고, 이해하고, 접근 방식을 정의하는 데 몇 시간.
설정 중에 pnpm은 수명 주기 스크립트가 있는 세 개의 패키지를 식별했습니다:
- esbuild: CLI 도구 시작 시간을 밀리초 단위로 최적화—JavaScript API만 사용하므로 필요 없음
- @firebase/util: 클라이언트 SDK 자동 구성—서버 SDK만 사용하므로 필요 없음
- protobufjs: 버전 스키마 호환성 검사—전이적 종속성이므로 필요 없음
우리는 각 스크립트가 무엇을 하는지 조사했고(문서를 읽고 스크립트를 AI에 넣어 해석을 요청), 우리 사용 사례에 필요한 것이 없다고 판단하여 모두 차단했습니다. 기능에는 전혀 영향이 없었습니다.
그게 전부였습니다. 몇 시간의 초기 투자로 Shai-Hulud 같은 공격에 대한 지속적인 보호를 얻었습니다.
마찰은 어떻게 느껴지는가: 이러한 제어는 의도적으로 마찰을 만들며—우리에게 이는 버그가 아니라 기능입니다. 이 마찰은 모든 것을 암묵적으로 신뢰하는 대신 우리 환경에서 어떤 코드가 실행될지에 대해 의식적인 결정을 내리도록 강제합니다. 새 종속성에 스크립트가 있을 때, 검토하고 결정을 문서화하는 데 약 15분이 걸릴 것으로 예상합니다.
프로세스에 익숙해지면서 연습을 통해 이 마찰이 더 직관적으로 느껴질 것으로 기대합니다.
우리가 배우고 있는 것#
시범 도입에서 배운 몇 가지:
심층 방어 모델은 실제로 효과가 있습니다. 클라이언트 측에 여러 계층이 있고—npm의 게시 측 개선의 혜택까지 더해지므로—예외에 대해 실용적으로 대처할 수 있습니다. 정당한 이유로 한 가지 제어를 우회해야 할 때도 나머지 제어가 여전히 우리를 보호합니다. 덕분에 예외를 만드는 데 대한 불안이 사라집니다—예외는 보안 실패가 아니라 시스템이 설계대로 동작하는 것입니다.
사고방식을 익히는 데는 시간이 걸립니다. "편의 우선"이 아니라 "보안 우선"으로 생각하는 데는 학습 곡선이 있습니다. 하지만 일단 사고방식이 자리 잡으면—조금 더 오래된 패키지가 더 안전하고, 명시적인 결정이 암묵적인 신뢰보다 낫다는 것—워크플로가 자연스럽게 느껴집니다.
이러한 제어는 중간 규모 팀에게도 실용적입니다. 우리는 전담 보안 팀이 있는 대형 기술 기업이 아닙니다. 우리는 엔지니어링 자원이 제한된 중간 규모의 뉴스 미디어 조직입니다. 우리가 이러한 제어를 성공적으로 도입할 수 있다면, 대부분의 팀도 할 수 있습니다.
우리는 여전히 배우는 중입니다. 위협 환경은 진화하며, 우리의 접근 방식도 그럴 것입니다. 신뢰 정책 기능은 나온 지 몇 주밖에 되지 않았으며, 실제로 정당한 신뢰 다운그레이드가 얼마나 자주 발생할지는 아직 모릅니다. 가까운 미래에 이러한 제어를 다른 코드베이스로 확대할 계획이며, 이를 통해 서로 다른 종속성 그래프를 가진 애플리케이션에서 어떻게 확장되는지에 대한 더 많은 데이터를 얻을 것입니다.
이를 고려하는 다른 팀을 위해#
만약 pnpm의 보안 제어를 고려하고 있다면, 우리에게 효과가 있었던 방법은 다음과 같습니다:
하나의 프로젝트로 시작하세요. 먼저 단일 코드베이스에서 시범 도입한 덕분에 더 넓은 도입을 고려하기 전에 워크플로에 익숙해지고, 마찰 지점을 이해하고, 자신감을 쌓을 수 있었습니다.
예외를 미리 계획하세요. 수명 주기 스크립트(컴파일이 필요한 패키지), 릴리스 대기 기간(중요한 보안 패치), 신뢰 다운그레이드(CI/CD 마이그레이션)에 대한 예외가 필요할 것이라고 예상하고 시작하세요. 이는 실패가 아니라—시스템이 설계된 방식대로 동작하는 것입니다.
첫날부터 strictDepBuilds: true를 사용하세요. 경고에 의존하는 것은 우리에게 너무 위험하게 느껴졌습니다. 우리는 설치가 즉시 실패하여 결정을 강제하기를 원했습니다. 이렇게 하면 패키지가 나중에 잘못 동작할 가능성을 막고 신중한 선택을 보장할 수 있습니다.
모든 예외를 문서화하세요. 수명 주기 스크립트를 허용하거나 패키지를 예외 처리한 이유를 기록하세요. 이는 감사 기록을 남기고, 향후 팀원이 그 이유를 이해하도록 돕고, 나중에 예외를 정리하기 쉽게 만듭니다.
계층을 신뢰하세요. 한 가지 제어에 예외를 둘 때, 나머지 두 가지가 여전히 여러분을 보호하고 있다는 것을 기억하세요. 심층 방어 모델은 실용적으로 대처할 여지를 줍니다.
경험을 공유해 주세요#
이러한 제어를 도입하고 있거나 고려 중인 다른 팀의 이야기를 듣고 싶습니다. 무엇이 효과가 있나요? 무엇이 어려운가요? 무엇을 배우셨나요? 여러분도 pnpm GitHub Discussions에서 대화에 참여하거나 소셜 미디어에서 경험을 공유해 주세요—우리 모두 함께 배우고 있습니다.
감사의 말#
이러한 제어를 만들고, 강력하면서도 실용적으로 만들기 위해 사려 깊게 접근해 준 pnpm 팀에게 감사드립니다. 그리고 우리의 이야기를 공유하도록 초대해 주셔서 감사합니다.
여러분이 하고 있는 일은 중요합니다. 이러한 제어는 npm의 레지스트리 개선을 보완하는 실질적인 보호를 제공합니다. 이 둘이 함께, 우리 같은 팀이 점점 더 정교해지는 공급망 공격에 맞설 수 있는 기회를 줍니다.
Ryan Sobol은 Seattle Times의 수석 소프트웨어 엔지니어로, 모바일 및 웹 개발, 클라우드 인프라, 개발자 도구를 담당하고 있습니다. 여기에 표현된 견해는 그 자신의 것이며, Seattle Times의 pnpm 보안 제어 시범 도입을 바탕으로 합니다.